From 8ab57bee2588fdd86411b4ba312c460f81d95870 Mon Sep 17 00:00:00 2001 From: Barnaby Walters Date: Sun, 6 Jun 2021 01:18:44 +0200 Subject: [PATCH] Initial commit Sketched out first draft of how the library should work, stubbed a lot of the smaller utility classes required, and outlined the main handler functions for the IA Server. --- .gitignore | 2 + README.md | 2 + composer.json | 24 + composer.lock | 1067 +++++++++++++++++ .../IndieAuth/ClosureRequestHandler.php | 22 + .../DoubleSubmitCookieCsrfMiddleware.php | 92 ++ .../IndieAuth/FilesystemJsonStorage.php | 68 ++ src/Taproot/IndieAuth/NoOpMiddleware.php | 14 + src/Taproot/IndieAuth/Server.php | 206 ++++ .../IndieAuth/TokenStorageInterface.php | 15 + 10 files changed, 1512 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 src/Taproot/IndieAuth/ClosureRequestHandler.php create mode 100644 src/Taproot/IndieAuth/DoubleSubmitCookieCsrfMiddleware.php create mode 100644 src/Taproot/IndieAuth/FilesystemJsonStorage.php create mode 100644 src/Taproot/IndieAuth/NoOpMiddleware.php create mode 100644 src/Taproot/IndieAuth/Server.php create mode 100644 src/Taproot/IndieAuth/TokenStorageInterface.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..47f36f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +vendor \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3d8c012 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# taproot/indieauth + diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..4623b0c --- /dev/null +++ b/composer.json @@ -0,0 +1,24 @@ +{ + "name": "taproot/indieauth", + "description": "PHP PSR-7-compliant IndieAuth Server and Client implementation.", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Barnaby Walters", + "email": "barnaby@waterpigs.co.uk" + } + ], + "require": { + "psr/http-message": "^1.0", + "psr/log": "^1.1", + "nyholm/psr7": "^1.4", + "indieauth/client": "^1.1", + "psr/http-client": "^1.0", + "psr/http-server-middleware": "^1.0", + "dflydev/fig-cookies": "^3.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.3" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..01e17a6 --- /dev/null +++ b/composer.lock @@ -0,0 +1,1067 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "e7b261990b1458b0da10c6ff5fa2a6d2", + "packages": [ + { + "name": "dflydev/fig-cookies", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/dflydev/dflydev-fig-cookies.git", + "reference": "ea6934204b1b34ffdf5130dc7e0928d18ced2498" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dflydev/dflydev-fig-cookies/zipball/ea6934204b1b34ffdf5130dc7e0928d18ced2498", + "reference": "ea6934204b1b34ffdf5130dc7e0928d18ced2498", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "php": "^7.2 || ^8.0", + "psr/http-message": "^1" + }, + "require-dev": { + "doctrine/coding-standard": "^8", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-phpunit": "^0.12.16", + "phpunit/phpunit": "^7.2.6 || ^9", + "scrutinizer/ocular": "^1.8", + "squizlabs/php_codesniffer": "^3.3", + "vimeo/psalm": "^4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Dflydev\\FigCookies\\": "src/Dflydev/FigCookies" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Beau Simensen", + "email": "beau@dflydev.com" + } + ], + "description": "Cookies for PSR-7 HTTP Message Interface.", + "keywords": [ + "cookies", + "psr-7", + "psr7" + ], + "support": { + "issues": "https://github.com/dflydev/dflydev-fig-cookies/issues", + "source": "https://github.com/dflydev/dflydev-fig-cookies/tree/v3.0.0" + }, + "time": "2021-01-22T02:53:56+00:00" + }, + { + "name": "indieauth/client", + "version": "1.1.5", + "source": { + "type": "git", + "url": "https://github.com/indieweb/indieauth-client-php.git", + "reference": "2ebd8396913ae8c72438dc24f037c8e1717b66ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/indieweb/indieauth-client-php/zipball/2ebd8396913ae8c72438dc24f037c8e1717b66ed", + "reference": "2ebd8396913ae8c72438dc24f037c8e1717b66ed", + "shasum": "" + }, + "require": { + "indieweb/representative-h-card": "^0.1.2", + "mf2/mf2": ">=0.3.2", + "p3k/http": ">=0.1.6", + "php": ">5.6.0" + }, + "require-dev": { + "phpunit/phpunit": "4.8.*" + }, + "type": "library", + "autoload": { + "psr-0": { + "IndieAuth": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Aaron Parecki", + "homepage": "https://aaronparecki.com" + } + ], + "description": "IndieAuth Client Library", + "support": { + "issues": "https://github.com/indieweb/indieauth-client-php/issues", + "source": "https://github.com/indieweb/indieauth-client-php/tree/1.1.5" + }, + "funding": [ + { + "url": "https://opencollective.com/indieweb", + "type": "opencollective" + } + ], + "time": "2021-01-10T00:19:07+00:00" + }, + { + "name": "indieweb/link-rel-parser", + "version": "0.1.3", + "source": { + "type": "git", + "url": "https://github.com/indieweb/link-rel-parser-php.git", + "reference": "295420e4f16d9a9d262a3c25a7a583794428f055" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/indieweb/link-rel-parser-php/zipball/295420e4f16d9a9d262a3c25a7a583794428f055", + "reference": "295420e4f16d9a9d262a3c25a7a583794428f055", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/IndieWeb/link_rel_parser.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Aaron Parecki", + "homepage": "http://aaronparecki.com" + }, + { + "name": "Tantek Çelik", + "homepage": "http://tantek.com" + } + ], + "description": "Parse rel values from HTTP headers", + "homepage": "https://github.com/indieweb/link-rel-parser-php", + "keywords": [ + "http", + "indieweb", + "microformats2" + ], + "support": { + "issues": "https://github.com/indieweb/link-rel-parser-php/issues", + "source": "https://github.com/indieweb/link-rel-parser-php/tree/master" + }, + "time": "2017-01-11T17:14:49+00:00" + }, + { + "name": "indieweb/representative-h-card", + "version": "0.1.2", + "source": { + "type": "git", + "url": "https://github.com/indieweb/representative-h-card-php.git", + "reference": "b70b01bd0dd7f2a940602137335dbf46ab6e2e38" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/indieweb/representative-h-card-php/zipball/b70b01bd0dd7f2a940602137335dbf46ab6e2e38", + "reference": "b70b01bd0dd7f2a940602137335dbf46ab6e2e38", + "shasum": "" + }, + "require": { + "php": ">=5.4" + }, + "require-dev": { + "mf2/mf2": "0.2.*", + "phpunit/phpunit": "*" + }, + "type": "library", + "autoload": { + "files": [ + "src/mf2/representative-h-card.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Aaron Parecki", + "homepage": "http://aaronparecki.com" + } + ], + "keywords": [ + "h-card", + "indieweb", + "mf2", + "microformats" + ], + "support": { + "issues": "https://github.com/indieweb/representative-h-card-php/issues", + "source": "https://github.com/indieweb/representative-h-card-php/tree/0.1.2" + }, + "time": "2015-12-23T18:11:19+00:00" + }, + { + "name": "mf2/mf2", + "version": "0.4.6", + "source": { + "type": "git", + "url": "https://github.com/microformats/php-mf2.git", + "reference": "00b70ee7eb7f5b0585b1bd467f6c9cbd75055d23" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/microformats/php-mf2/zipball/00b70ee7eb7f5b0585b1bd467f6c9cbd75055d23", + "reference": "00b70ee7eb7f5b0585b1bd467f6c9cbd75055d23", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "mf2/tests": "@dev", + "phpdocumentor/phpdocumentor": "v2.8.4", + "phpunit/phpunit": "4.8.*" + }, + "suggest": { + "barnabywalters/mf-cleaner": "To more easily handle the canonical data php-mf2 gives you", + "masterminds/html5": "Alternative HTML parser for PHP, for better HTML5 support." + }, + "bin": [ + "bin/fetch-mf2", + "bin/parse-mf2" + ], + "type": "library", + "autoload": { + "files": [ + "Mf2/Parser.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "CC0-1.0" + ], + "authors": [ + { + "name": "Barnaby Walters", + "homepage": "http://waterpigs.co.uk" + } + ], + "description": "A pure, generic microformats2 parser — makes HTML as easy to consume as a JSON API", + "keywords": [ + "html", + "microformats", + "microformats 2", + "parser", + "semantic" + ], + "support": { + "issues": "https://github.com/microformats/php-mf2/issues", + "source": "https://github.com/microformats/php-mf2/tree/master" + }, + "time": "2018-08-24T14:47:04+00:00" + }, + { + "name": "nyholm/psr7", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/Nyholm/psr7.git", + "reference": "23ae1f00fbc6a886cbe3062ca682391b9cc7c37b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/23ae1f00fbc6a886cbe3062ca682391b9cc7c37b", + "reference": "23ae1f00fbc6a886cbe3062ca682391b9cc7c37b", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "php-http/message-factory": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "http-interop/http-factory-tests": "^0.8", + "php-http/psr7-integration-tests": "^1.0", + "phpunit/phpunit": "^7.5 || 8.5 || 9.4", + "symfony/error-handler": "^4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "Nyholm\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" + } + ], + "description": "A fast PHP7 implementation of PSR-7", + "homepage": "https://tnyholm.se", + "keywords": [ + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/Nyholm/psr7/issues", + "source": "https://github.com/Nyholm/psr7/tree/1.4.0" + }, + "funding": [ + { + "url": "https://github.com/Zegnat", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2021-02-18T15:41:32+00:00" + }, + { + "name": "p3k/http", + "version": "0.1.11", + "source": { + "type": "git", + "url": "https://github.com/aaronpk/p3k-http.git", + "reference": "24d28287e0c5606aa45b23c6e3c17628d89468f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aaronpk/p3k-http/zipball/24d28287e0c5606aa45b23c6e3c17628d89468f7", + "reference": "24d28287e0c5606aa45b23c6e3c17628d89468f7", + "shasum": "" + }, + "require": { + "indieweb/link-rel-parser": "0.1.*", + "mf2/mf2": ">=0.3.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "p3k\\": "src/p3k" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Parecki", + "homepage": "https://aaronparecki.com" + } + ], + "description": "A simple wrapper API around the PHP curl functions", + "homepage": "https://github.com/aaronpk/p3k-http", + "support": { + "issues": "https://github.com/aaronpk/p3k-http/issues", + "source": "https://github.com/aaronpk/p3k-http/tree/master" + }, + "time": "2020-02-19T03:00:09+00:00" + }, + { + "name": "php-http/message-factory", + "version": "v1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-http/message-factory.git", + "reference": "a478cb11f66a6ac48d8954216cfed9aa06a501a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/message-factory/zipball/a478cb11f66a6ac48d8954216cfed9aa06a501a1", + "reference": "a478cb11f66a6ac48d8954216cfed9aa06a501a1", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Factory interfaces for PSR-7 HTTP Message", + "homepage": "http://php-http.org", + "keywords": [ + "factory", + "http", + "message", + "stream", + "uri" + ], + "support": { + "issues": "https://github.com/php-http/message-factory/issues", + "source": "https://github.com/php-http/message-factory/tree/master" + }, + "time": "2015-12-19T14:08:53+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client/tree/master" + }, + "time": "2020-06-29T06:28:15+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "shasum": "" + }, + "require": { + "php": ">=7.0.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory/tree/master" + }, + "time": "2019-04-30T12:38:16+00:00" + }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/master" + }, + "time": "2016-08-06T14:39:51+00:00" + }, + { + "name": "psr/http-server-handler", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-handler.git", + "reference": "aff2f80e33b7f026ec96bb42f63242dc50ffcae7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/aff2f80e33b7f026ec96bb42f63242dc50ffcae7", + "reference": "aff2f80e33b7f026ec96bb42f63242dc50ffcae7", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side request handler", + "keywords": [ + "handler", + "http", + "http-interop", + "psr", + "psr-15", + "psr-7", + "request", + "response", + "server" + ], + "support": { + "issues": "https://github.com/php-fig/http-server-handler/issues", + "source": "https://github.com/php-fig/http-server-handler/tree/master" + }, + "time": "2018-10-30T16:46:14+00:00" + }, + { + "name": "psr/http-server-middleware", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-middleware.git", + "reference": "2296f45510945530b9dceb8bcedb5cb84d40c5f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/2296f45510945530b9dceb8bcedb5cb84d40c5f5", + "reference": "2296f45510945530b9dceb8bcedb5cb84d40c5f5", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0", + "psr/http-server-handler": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side middleware", + "keywords": [ + "http", + "http-interop", + "middleware", + "psr", + "psr-15", + "psr-7", + "request", + "response" + ], + "support": { + "issues": "https://github.com/php-fig/http-server-middleware/issues", + "source": "https://github.com/php-fig/http-server-middleware/tree/master" + }, + "time": "2018-10-30T17:12:04+00:00" + }, + { + "name": "psr/log", + "version": "1.1.4", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.4" + }, + "time": "2021-05-03T11:20:27+00:00" + } + ], + "packages-dev": [ + { + "name": "guzzlehttp/guzzle", + "version": "7.3.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "7008573787b430c1c1f650e3722d9bba59967628" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7008573787b430c1c1f650e3722d9bba59967628", + "reference": "7008573787b430c1c1f650e3722d9bba59967628", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.4", + "guzzlehttp/psr7": "^1.7 || ^2.0", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "ext-curl": "*", + "php-http/client-integration-tests": "^3.0", + "phpunit/phpunit": "^8.5.5 || ^9.3.5", + "psr/log": "^1.1" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.3-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.3.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://github.com/alexeyshockov", + "type": "github" + }, + { + "url": "https://github.com/gmponos", + "type": "github" + } + ], + "time": "2021-03-23T11:33:13+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/8e7d04f1f6450fef59366c399cfad4b9383aa30d", + "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "symfony/phpunit-bridge": "^4.4 || ^5.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/1.4.1" + }, + "time": "2021-03-07T09:25:29+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "1.8.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "dc960a912984efb74d0a90222870c72c87f10c91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/dc960a912984efb74d0a90222870c72c87f10c91", + "reference": "dc960a912984efb74d0a90222870c72c87f10c91", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5 || ^3.0.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "ext-zlib": "*", + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.7-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Schultze", + "homepage": "https://github.com/Tobion" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/1.8.2" + }, + "time": "2021-04-26T09:17:50+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.0.0" +} diff --git a/src/Taproot/IndieAuth/ClosureRequestHandler.php b/src/Taproot/IndieAuth/ClosureRequestHandler.php new file mode 100644 index 0000000..9559697 --- /dev/null +++ b/src/Taproot/IndieAuth/ClosureRequestHandler.php @@ -0,0 +1,22 @@ +callable = $callable; + $this->args = array_slice(func_get_args(), 1); + } + + public function handle(ServerRequestInterface $request): ResponseInterface { + return call_user_func_array($this->callable, array_merge([$request], $this->args)); + } +} diff --git a/src/Taproot/IndieAuth/DoubleSubmitCookieCsrfMiddleware.php b/src/Taproot/IndieAuth/DoubleSubmitCookieCsrfMiddleware.php new file mode 100644 index 0000000..c78b988 --- /dev/null +++ b/src/Taproot/IndieAuth/DoubleSubmitCookieCsrfMiddleware.php @@ -0,0 +1,92 @@ +attribute = $attribute; + $this->ttl = $ttl; + $this->tokenLength = $tokenLength; + + if (!is_callable($errorResponse)) { + if (!$errorResponse instanceof ResponseInterface) { + if (!is_string($errorResponse)) { + $errorResponse = self::DEFAULT_ERROR_RESPONSE_STRING; + } + $errorResponse = new Response(400, ['content-type' => 'text/plain'], $errorResponse); + } + $errorResponse = function (ServerRequestInterface $request) use ($errorResponse) { return $errorResponse; }; + } + $this->errorResponse = $errorResponse; + } + + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { + if (!in_array(strtoupper($request->getMethod()), self::READ_METHODS)) { + // This request is a write method and requires CSRF protection. + if (!$this->isValid($request)) { + return call_user_func($this->errorResponse, $request); + } + } + + // Otherwise, generate a new CSRF token, add it to the request attributes, and as a cookie on the response. + $csrfToken = generateRandomString($this->tokenLength); + $request = $request->withAttribute($this->attribute, $csrfToken); + + $response = $handler->handle($request); + + // Add the new CSRF cookie, restricting its scope to match the current request. + $response = FigCookies\FigResponseCookies::set($response, FigCookies\SetCookie::create($this->attribute) + ->withValue($csrfToken) + ->withMaxAge($this->ttl) + ->withSecure($request->getUri()->getScheme() == 'https') + ->withDomain($request->getUri()->getHost()) + ->withPath($request->getUri()->getPath())); + + return $response; + } + + protected function isValid(ServerRequestInterface $request) { + if (in_array($this->attribute, $request->getParsedBody())) { + if (in_array($this->attribute, $request->getCookieParams())) { + return hash_equals($request->getParsedBody()[$this->attribute], $request->getCookieParams()[$this->attribute]); + } + } + return false; + } +} diff --git a/src/Taproot/IndieAuth/FilesystemJsonStorage.php b/src/Taproot/IndieAuth/FilesystemJsonStorage.php new file mode 100644 index 0000000..ddc50b2 --- /dev/null +++ b/src/Taproot/IndieAuth/FilesystemJsonStorage.php @@ -0,0 +1,68 @@ +path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; + $this->ttl = $ttl; + + if ($cleanUpNow) { + $this->cleanUp(); + } + } + + public function cleanUp($ttl=null): int { + $ttl = $ttl ?? $this->ttl; + + $deleted = 0; + + // A TTL of 0 means the token should live until deleted, and negative TTLs are invalid. + if ($ttl >= 0) { + foreach (new DirectoryIterator($this->path) as $fileInfo) { + if ($fileInfo->isFile() && time() - max($fileInfo->getMTime(), $fileInfo->getCTime()) > $ttl) { + unlink($fileInfo->getPathname()); + $deleted++; + } + } + } + + return $deleted; + } + + public function get(string $key): ?array { + $path = $this->getPath($key); + if (file_exists($path)) { + $result = json_decode(file_get_contents($path), true); + + if (is_array($result)) { + return $result; + } + } + + return null; + } + + public function put(string $key, array $data): bool { + // Ensure that the containing folder exists. + mkdir($this->path, 0777, true); + + return file_put_contents($this->getPath($key), json_encode($data)) !== false; + } + + public function delete(string $key): bool { + if (file_exists($this->getPath($key))) { + return unlink($this->getPath($key)); + } + return false; + } + + public function getPath(string $key): string { + return $this->path . "$key.json"; + } +} diff --git a/src/Taproot/IndieAuth/NoOpMiddleware.php b/src/Taproot/IndieAuth/NoOpMiddleware.php new file mode 100644 index 0000000..41fa786 --- /dev/null +++ b/src/Taproot/IndieAuth/NoOpMiddleware.php @@ -0,0 +1,14 @@ +handle($request); + } +} \ No newline at end of file diff --git a/src/Taproot/IndieAuth/Server.php b/src/Taproot/IndieAuth/Server.php new file mode 100644 index 0000000..ffe80c4 --- /dev/null +++ b/src/Taproot/IndieAuth/Server.php @@ -0,0 +1,206 @@ +getMethod()) == 'post' + && array_key_exists('grant_type', $request->getParsedBody()) + && $request->getParsedBody()['grant_type'] == 'authorization_code'; +} + +function isIndieAuthAuthorizationRequest(ServerRequestInterface $request, $permittedMethods=['get']) { + return in_array(strtolower($request->getMethod()), array_map('strtolower', $permittedMethods)) + && array_key_exists('response_type', $request->getQueryParams()) + && $request->getQueryParams()['response_type'] == 'code'; +} + +function isAuthorizationApprovalRequest(ServerRequestInterface $request) { + return strtolower($request->getMethod()) == 'post' + && array_key_exists('taproot_indieauth_action', $request->getParsedBody()) + && $request->getParsedBody()['taproot_indieauth_action'] == 'approve'; +} + +function buildQueryString(array $parameters) { + $qs = []; + foreach ($parameters as $k => $v) { + $qs[] = urlencode($k) . '=' . urlencode($v); + } + return join('&', $qs); +} + +class Server { + const CUSTOMIZE_AUTHORIZATION_CODE = 'customise_authorization_code'; + const SHOW_AUTHORIZATION_PAGE = 'show_authorization_page'; + const HANDLE_NON_INDIEAUTH_REQUEST = 'handle_non_indieauth_request'; + const HANDLE_AUTHENTICATION_REQUEST = 'handle_authentication_request'; + const DEFAULT_CSRF_KEY = 'taproot_indieauth_server_csrf'; + + public $callbacks; + + public TokenStorageInterface $authorizationCodeStorage; + + public TokenStorageInterface $accessTokenStorage; + + public MiddlewareInterface $csrfMiddleware; + + public LoggerInterface $logger; + + public string $csrfKey; + + public function __construct(array $callbacks, $authorizationCodeStorage, $accessTokenStorage, $csrfMiddleware=null, string $csrfKey=self::DEFAULT_CSRF_KEY, LoggerInterface $logger=null) { + $callbacks = array_merge([ + self::CUSTOMIZE_AUTHORIZATION_CODE => function (array $code) { return $code; }, // Default to no-op. + self::SHOW_AUTHORIZATION_PAGE => function (ServerRequestInterface $request, array $authenticationResult, string $authenticationRedirect) { }, // TODO: Put the default implementation here. + self::HANDLE_NON_INDIEAUTH_REQUEST => function (ServerRequestInterface $request) { return null; }, // Default to no-op. + ], $callbacks); + + if (!(array_key_exists(self::HANDLE_AUTHENTICATION_REQUEST, $callbacks) and is_callable($callbacks[self::HANDLE_AUTHENTICATION_REQUEST]))) { + throw new Exception('$callbacks[\'' . self::HANDLE_AUTHENTICATION_REQUEST .'\'] must be present and callable.'); + } + $this->callbacks = $callbacks; + + if (!$authorizationCodeStorage instanceof TokenStorageInterface) { + if (is_string($authorizationCodeStorage)) { + $authorizationCodeStorage = new FilesystemJsonStorage($authorizationCodeStorage, 600, true); + } else { + throw new Exception('$authorizationCodeStorage parameter must be either a string (path) or an instance of Taproot\IndieAuth\TokenStorageInterface.'); + } + } + $this->authorizationCodeStorage = $authorizationCodeStorage; + + if (!$accessTokenStorage instanceof TokenStorageInterface) { + if (is_string($accessTokenStorage)) { + // Create a default access token storage with a TTL of 7 days. + $accessTokenStorage = new FilesystemJsonStorage($accessTokenStorage, 60 * 60 * 24 * 7, true); + } else { + throw new Exception('$accessTokenStorage parameter must be either a string (path) or an instance of Taproot\IndieAuth\TokenStorageInterface.'); + } + } + $this->accessTokenStorage = $accessTokenStorage; + + $this->csrfKey = $csrfKey; + + if (!$csrfMiddleware instanceof MiddlewareInterface) { + // Default to the statless Double-Submit Cookie CSRF Middleware, with default settings. + $csrfMiddleware = new DoubleSubmitCookieCsrfMiddleware($this->csrfKey); + } + $this->csrfMiddleware = $csrfMiddleware; + + $this->logger = $logger ?? new NullLogger(); + } + + public function handleAuthorizationEndpointRequest(ServerRequestInterface $request): ResponseInterface { + // If it’s a profile information request: + if (isIndieAuthAuthorizationCodeRedeemingRequest($request)) { + // Verify that the authorization code is valid and has not yet been used. + $this->authorizationCodeStorage->get($request->getParsedBody()['code']); + + // Verify that it was issued for the same client_id and redirect_uri + + // Check that the supplied code_verifier hashes to the stored code_challenge + + // If everything checked out, return {"me": "https://example.com"} response + // (a response containing any additional information must contain a valid scope value, and + // be handled by the token_endpoint). + // TODO: according to the spec, it is technically permitted for the authorization endpoint + // to additional provide profile information. Leave it up to the library consumer to decide + // whether to add it or not. + } + + // Because the special case above isn’t allowed to be CSRF-protected, we have to do some rather silly + // gymnastics here to selectively-CSRF-protect requests which do need it. + return $this->csrfMiddleware->process($request, new ClosureRequestHandler(function (ServerRequestInterface $request) { + // If this is an authorization or approval request (allowing POST requests as well to accommodate + // approval requests and custom auth form submission. + if (isIndieAuthAuthorizationRequest($request, ['get', 'post'])) { + // Build a URL for the authentication flow to redirect to, if it needs to. + // TODO: perhaps filter queryParams to only include the indieauth-relevant params? + $authenticationRedirect = $request->getUri() . '?' . buildQueryString($request->getQueryParams()); + + $authenticationResult = call_user_func($this->callbacks[self::HANDLE_AUTHENTICATION_REQUEST], $request, $authenticationRedirect); + + // If the authentication handler returned a Response, return that as-is. + if ($authenticationResult instanceof Response) { + return $authenticationResult; + } elseif (is_array($authenticationResult)) { + // Check the resulting array for errors. + + // The user is logged in. + + // If this is a POST request sent from the authorization (i.e. scope-choosing) form: + if (isAuthorizationApprovalRequest($request)) { + // Assemble the data for the authorization code, store it somewhere persistent. + $code = [ + + ]; + + // Pass it to the auth code customisation callback, if any. + $code = call_user_func($this->callbacks[self::CUSTOMIZE_AUTHORIZATION_CODE], $code, $request); + + // Store the authorization code. + $this->authorizationCodeStorage->put($code['code'], $code); + + // Return a redirect to the client app. + } + + // Otherwise, the user is authenticated and needs to authorize the client app + choose scopes. + + // Fetch the client_id URL to find information about the client to present to the user. + + // If the authority of the redirect_uri does not match the client_id or one of their redirect URLs, return an error. + + // Present the authorization UI. + return call_user_func($this->callbacks[self::SHOW_AUTHORIZATION_PAGE], $request, $authenticationResult, $authenticationRedirect); + } + } + + // If the request isn’t an IndieAuth Authorization or Code-redeeming request, it’s either an invalid + // request or something to do with a custom auth handler (e.g. sending a one-time code in an email.) + $nonIndieAuthRequestResult = call_user_func($this->callbacks[self::HANDLE_NON_INDIEAUTH_REQUEST], $request); + if ($nonIndieAuthRequestResult instanceof ResponseInterface) { + return $nonIndieAuthRequestResult; + } else { + return new Response(400, ['content-type' => 'application/json'], json_encode([ + 'error' => 'invalid_request' + ])); + } + })); + } + + public function handleTokenEndpointRequest(ServerRequestInterface $request): ResponseInterface { + // This is a request to redeem an authorization_code for an access_token. + + // Verify that the authorization code is valid and has not yet been used. + + // Verify that it was issued for the same client_id and redirect_uri + + // Check that the supplied code_verifier hashes to the stored code_challenge + + // If the auth code was issued with no scope, return an error. + + // If everything checks out, generate an access token and return it. + } +} diff --git a/src/Taproot/IndieAuth/TokenStorageInterface.php b/src/Taproot/IndieAuth/TokenStorageInterface.php new file mode 100644 index 0000000..864d316 --- /dev/null +++ b/src/Taproot/IndieAuth/TokenStorageInterface.php @@ -0,0 +1,15 @@ +