Finished first draft of the authorization endpoint implementation
Made some minor tweaks and improvements to the utility classes Required some new dependencies Server::__construct now takes a single config array as the list of parameters was getting rather long.
This commit is contained in:
parent
8ab57bee25
commit
1b2bd1f513
@ -16,7 +16,10 @@
|
||||
"indieauth/client": "^1.1",
|
||||
"psr/http-client": "^1.0",
|
||||
"psr/http-server-middleware": "^1.0",
|
||||
"dflydev/fig-cookies": "^3.0"
|
||||
"dflydev/fig-cookies": "^3.0",
|
||||
"mf2/mf2": "^0.4.6",
|
||||
"barnabywalters/mf-cleaner": "^0.1.4",
|
||||
"guzzlehttp/psr7": "^1.8"
|
||||
},
|
||||
"require-dev": {
|
||||
"guzzlehttp/guzzle": "^7.3"
|
||||
|
284
composer.lock
generated
284
composer.lock
generated
@ -4,8 +4,52 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "e7b261990b1458b0da10c6ff5fa2a6d2",
|
||||
"content-hash": "a3c5834a1f1214cdf5ec23a831b9c2cf",
|
||||
"packages": [
|
||||
{
|
||||
"name": "barnabywalters/mf-cleaner",
|
||||
"version": "v0.1.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/barnabywalters/php-mf-cleaner.git",
|
||||
"reference": "ef6a16628db6e8aee2b4f8bb8093d18c24b74cd4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/barnabywalters/php-mf-cleaner/zipball/ef6a16628db6e8aee2b4f8bb8093d18c24b74cd4",
|
||||
"reference": "ef6a16628db6e8aee2b4f8bb8093d18c24b74cd4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require-dev": {
|
||||
"php": ">=5.3",
|
||||
"phpunit/phpunit": "*"
|
||||
},
|
||||
"suggest": {
|
||||
"mf2/mf2": "To parse microformats2 structures from (X)HTML"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/BarnabyWalters/Mf2/Functions.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Barnaby Walters",
|
||||
"email": "barnaby@waterpigs.co.uk"
|
||||
}
|
||||
],
|
||||
"description": "Cleans up microformats2 array structures",
|
||||
"support": {
|
||||
"issues": "https://github.com/barnabywalters/php-mf-cleaner/issues",
|
||||
"source": "https://github.com/barnabywalters/php-mf-cleaner/tree/v0.1.4"
|
||||
},
|
||||
"time": "2014-10-06T23:11:15+00:00"
|
||||
},
|
||||
{
|
||||
"name": "dflydev/fig-cookies",
|
||||
"version": "v3.0.0",
|
||||
@ -68,6 +112,81 @@
|
||||
},
|
||||
"time": "2021-01-22T02:53:56+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": "indieauth/client",
|
||||
"version": "1.1.5",
|
||||
@ -775,6 +894,50 @@
|
||||
"source": "https://github.com/php-fig/log/tree/1.1.4"
|
||||
},
|
||||
"time": "2021-05-03T11:20:27+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"
|
||||
}
|
||||
],
|
||||
"packages-dev": [
|
||||
@ -935,125 +1098,6 @@
|
||||
"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": [],
|
||||
|
@ -8,6 +8,9 @@ use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Dflydev\FigCookies;
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
|
||||
// From https://github.com/indieweb/indieauth-client-php/blob/main/src/IndieAuth/Client.php, thanks aaronpk.
|
||||
function generateRandomString($numBytes) {
|
||||
@ -24,7 +27,7 @@ function generateRandomString($numBytes) {
|
||||
return bin2hex($bytes);
|
||||
}
|
||||
|
||||
class DoubleSubmitCookieCsrfMiddleware implements MiddlewareInterface {
|
||||
class DoubleSubmitCookieCsrfMiddleware implements MiddlewareInterface, LoggerAwareInterface {
|
||||
const READ_METHODS = ['HEAD', 'GET', 'OPTIONS'];
|
||||
const TTL = 60 * 20;
|
||||
const ATTRIBUTE = 'csrf';
|
||||
@ -39,7 +42,9 @@ class DoubleSubmitCookieCsrfMiddleware implements MiddlewareInterface {
|
||||
|
||||
public int $tokenLength;
|
||||
|
||||
public function __construct(string $attribute=self::ATTRIBUTE, int $ttl=self::TTL, $errorResponse=self::DEFAULT_ERROR_RESPONSE_STRING, $tokenLength=self::CSRF_TOKEN_LENGTH) {
|
||||
public LoggerInterface $logger;
|
||||
|
||||
public function __construct(string $attribute=self::ATTRIBUTE, int $ttl=self::TTL, $errorResponse=self::DEFAULT_ERROR_RESPONSE_STRING, $tokenLength=self::CSRF_TOKEN_LENGTH, $logger=null) {
|
||||
$this->attribute = $attribute;
|
||||
$this->ttl = $ttl;
|
||||
$this->tokenLength = $tokenLength;
|
||||
@ -54,6 +59,15 @@ class DoubleSubmitCookieCsrfMiddleware implements MiddlewareInterface {
|
||||
$errorResponse = function (ServerRequestInterface $request) use ($errorResponse) { return $errorResponse; };
|
||||
}
|
||||
$this->errorResponse = $errorResponse;
|
||||
|
||||
if (!$logger instanceof LoggerInterface) {
|
||||
$logger = new NullLogger();
|
||||
}
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
public function setLogger(LoggerInterface $logger) {
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
|
||||
|
@ -23,9 +23,9 @@ class FilesystemJsonStorage implements TokenStorageInterface {
|
||||
$deleted = 0;
|
||||
|
||||
// A TTL of 0 means the token should live until deleted, and negative TTLs are invalid.
|
||||
if ($ttl >= 0) {
|
||||
if ($ttl > 0) {
|
||||
foreach (new DirectoryIterator($this->path) as $fileInfo) {
|
||||
if ($fileInfo->isFile() && time() - max($fileInfo->getMTime(), $fileInfo->getCTime()) > $ttl) {
|
||||
if ($fileInfo->isFile() && $fileInfo->getExtension() == 'json' && time() - max($fileInfo->getMTime(), $fileInfo->getCTime()) > $ttl) {
|
||||
unlink($fileInfo->getPathname());
|
||||
$deleted++;
|
||||
}
|
||||
|
@ -7,8 +7,14 @@ use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
|
||||
/**
|
||||
* No-Op Middleware
|
||||
*
|
||||
* A PSR-15 Middleware which does nothing, simply passing `$request` onto `$handler` and returning
|
||||
* the response.
|
||||
*/
|
||||
class NoOpMiddleware implements MiddlewareInterface {
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
|
||||
return $handler->handle($request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,15 +3,27 @@
|
||||
namespace Taproot\IndieAuth;
|
||||
|
||||
use Exception;
|
||||
use GuzzleHttp\Exception\ServerException;
|
||||
use IndieAuth\Client as IndieAuthClient;
|
||||
use Mf2;
|
||||
use BarnabyWalters\Mf2 as M;
|
||||
use GuzzleHttp\Psr7\Header as HeaderParser;
|
||||
use Nyholm\Psr7\Response;
|
||||
use GuzzleHttp\Psr7\ServerRequest;
|
||||
use Nyholm\Psr7\Request;
|
||||
use Nyholm\Psr7\ServerRequest;
|
||||
use Psr\Http\Client\ClientExceptionInterface;
|
||||
use Psr\Http\Client\ClientInterface as HttpClientInterface;
|
||||
use Psr\Http\Client\NetworkExceptionInterface;
|
||||
use Psr\Http\Client\RequestExceptionInterface;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use IndieAuth\Client as IndieAuthClient;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
|
||||
use function PHPSTORM_META\type;
|
||||
|
||||
/**
|
||||
* Development Reference
|
||||
@ -51,6 +63,26 @@ function buildQueryString(array $parameters) {
|
||||
return join('&', $qs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Append Query Parameters
|
||||
*
|
||||
* Converts `$queryParams` into a query string, then checks `$uri` for an
|
||||
* existing query string. Then appends the newly generated query string
|
||||
* with either ? or & as appropriate.
|
||||
*/
|
||||
function appendQueryParams(string $uri, array $queryParams) {
|
||||
$queryString = buildQueryString($queryParams);
|
||||
$separator = parse_url($uri, \PHP_URL_QUERY) ? '&' : '?';
|
||||
return "{$uri}{$separator}{$queryString}";
|
||||
}
|
||||
|
||||
function trySetLogger($target, LoggerInterface $logger) {
|
||||
if ($target instanceof LoggerAwareInterface) {
|
||||
$target->setLogger($logger);
|
||||
}
|
||||
return $target;
|
||||
}
|
||||
|
||||
class Server {
|
||||
const CUSTOMIZE_AUTHORIZATION_CODE = 'customise_authorization_code';
|
||||
const SHOW_AUTHORIZATION_PAGE = 'show_authorization_page';
|
||||
@ -68,20 +100,39 @@ class Server {
|
||||
|
||||
public LoggerInterface $logger;
|
||||
|
||||
public HttpClientInterface $httpClient;
|
||||
|
||||
public callable $httpGetWithEffectiveUrl;
|
||||
|
||||
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);
|
||||
public function __construct(array $config) {
|
||||
$config = array_merge_recursive([
|
||||
'csrfMiddleware' => null,
|
||||
'csrfKey' => self::DEFAULT_CSRF_KEY,
|
||||
'logger' => null,
|
||||
'callbacks' => [
|
||||
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.
|
||||
],
|
||||
'authorizationCodeStorage' => null,
|
||||
'accessTokenStorage' => null,
|
||||
'httpGetWithEffectiveUrl' => null
|
||||
], $config);
|
||||
|
||||
if (!$config['logger'] instanceof LoggerInterface) {
|
||||
throw new Exception("\$config['logger'] must be an instance of \\Psr\\Log\\LoggerInterface or null.");
|
||||
}
|
||||
$this->logger = $config['logger'] ?? new NullLogger();
|
||||
|
||||
$callbacks = $config['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;
|
||||
|
||||
|
||||
$authorizationCodeStorage = $config['authorizationCodeStorage'];
|
||||
if (!$authorizationCodeStorage instanceof TokenStorageInterface) {
|
||||
if (is_string($authorizationCodeStorage)) {
|
||||
$authorizationCodeStorage = new FilesystemJsonStorage($authorizationCodeStorage, 600, true);
|
||||
@ -89,8 +140,10 @@ class Server {
|
||||
throw new Exception('$authorizationCodeStorage parameter must be either a string (path) or an instance of Taproot\IndieAuth\TokenStorageInterface.');
|
||||
}
|
||||
}
|
||||
trySetLogger($authorizationCodeStorage, $this->logger);
|
||||
$this->authorizationCodeStorage = $authorizationCodeStorage;
|
||||
|
||||
$accessTokenStorage = $config['accessTokenStorage'];
|
||||
if (!$accessTokenStorage instanceof TokenStorageInterface) {
|
||||
if (is_string($accessTokenStorage)) {
|
||||
// Create a default access token storage with a TTL of 7 days.
|
||||
@ -99,22 +152,51 @@ class Server {
|
||||
throw new Exception('$accessTokenStorage parameter must be either a string (path) or an instance of Taproot\IndieAuth\TokenStorageInterface.');
|
||||
}
|
||||
}
|
||||
trySetLogger($accessTokenStorage, $this->logger);
|
||||
$this->accessTokenStorage = $accessTokenStorage;
|
||||
|
||||
$this->csrfKey = $csrfKey;
|
||||
$this->csrfKey = $config['csrfKey'];
|
||||
|
||||
$csrfMiddleware = $config['csrfMiddleware'];
|
||||
if (!$csrfMiddleware instanceof MiddlewareInterface) {
|
||||
// Default to the statless Double-Submit Cookie CSRF Middleware, with default settings.
|
||||
$csrfMiddleware = new DoubleSubmitCookieCsrfMiddleware($this->csrfKey);
|
||||
}
|
||||
trySetLogger($csrfMiddleware, $this->logger);
|
||||
$this->csrfMiddleware = $csrfMiddleware;
|
||||
|
||||
$this->logger = $logger ?? new NullLogger();
|
||||
$httpGetWithEffectiveUrl = $config['httpGetWithEffectiveUrl'];
|
||||
if (!is_callable($httpGetWithEffectiveUrl)) {
|
||||
if (class_exists('\GuzzleHttp\Client')) {
|
||||
$httpGetWithEffectiveUrl = function (string $uri) {
|
||||
$resp = (new \GuzzleHttp\Client([
|
||||
\GuzzleHttp\RequestOptions::ALLOW_REDIRECTS => [
|
||||
'max' => 10,
|
||||
'strict' => true,
|
||||
'referer' => true,
|
||||
'track_redirects' => true
|
||||
]
|
||||
]))->get($uri);
|
||||
|
||||
$rdh = $resp->getHeader('X-Guzzle-Redirect-History');
|
||||
$effectiveUrl = empty($rdh) ? $uri : array_values($rdh)[count($rdh) - 1];
|
||||
|
||||
return [$resp, $effectiveUrl];
|
||||
};
|
||||
} else {
|
||||
throw new Exception('No valid $httpGetWithEffectiveUrl was provided, and guzzlehttp/guzzle was not installed. Either require guzzlehttp/guzzle, or provide a valid callable.');
|
||||
}
|
||||
}
|
||||
trySetLogger($httpGetWithEffectiveUrl, $this->logger);
|
||||
$this->httpGetWithEffectiveUrl = $httpGetWithEffectiveUrl;
|
||||
}
|
||||
|
||||
public function handleAuthorizationEndpointRequest(ServerRequestInterface $request): ResponseInterface {
|
||||
$this->logger->info('Handling an IndieAuth Authorization Endpoint request.');
|
||||
|
||||
// If it’s a profile information request:
|
||||
if (isIndieAuthAuthorizationCodeRedeemingRequest($request)) {
|
||||
$this->logger->info('Handling a request to redeem an authorization code for profile information.');
|
||||
// Verify that the authorization code is valid and has not yet been used.
|
||||
$this->authorizationCodeStorage->get($request->getParsedBody()['code']);
|
||||
|
||||
@ -136,10 +218,30 @@ class Server {
|
||||
// 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'])) {
|
||||
$this->logger->info('Handling an authorization request', ['method' => $request->getMethod()]);
|
||||
|
||||
$queryParams = $request->getQueryParams();
|
||||
// Return an error if we’re missing required parameters.
|
||||
$requiredParameters = ['client_id', 'redirect_uri', 'state', 'code_challenge', 'code_challenge_method'];
|
||||
$missingRequiredParameters = array_filter($requiredParameters, function ($p) use ($queryParams) {
|
||||
return !array_key_exists($p, $queryParams) || empty($queryParams[$p]);
|
||||
});
|
||||
if (!empty($missingRequiredParameters)) {
|
||||
$this->logger->warning('The authorization request was missing required parameters. Returning an error response.', ['missing' => $missingRequiredParameters]);
|
||||
// TODO: return a better response, or at least allow the library consumer to configure a better response.
|
||||
return new Response(400, ['content-type' => 'text/plain'], 'The IndieAuth request was missing the following parameters: ' . join("\n", $missingRequiredParameters));
|
||||
}
|
||||
|
||||
// Normalise the me parameter, if it exists.
|
||||
if (array_key_exists('me', $queryParams)) {
|
||||
$queryParams['me'] = IndieAuthClient::normalizeMeURL($queryParams['me']);
|
||||
}
|
||||
|
||||
// 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());
|
||||
$authenticationRedirect = $request->getUri() . '?' . buildQueryString($queryParams);
|
||||
|
||||
$this->logger->info('Calling handle_authentication_request callback');
|
||||
$authenticationResult = call_user_func($this->callbacks[self::HANDLE_AUTHENTICATION_REQUEST], $request, $authenticationRedirect);
|
||||
|
||||
// If the authentication handler returned a Response, return that as-is.
|
||||
@ -147,15 +249,77 @@ class Server {
|
||||
return $authenticationResult;
|
||||
} elseif (is_array($authenticationResult)) {
|
||||
// Check the resulting array for errors.
|
||||
if (!array_key_exists('me', $authenticationResult)) {
|
||||
$this->logger->error('The handle_authentication_request callback returned an array with no me key.', ['array' => $authenticationResult]);
|
||||
return new Response(500, ['content-type' => 'text/plain'], 'An internal error occurred.');
|
||||
}
|
||||
|
||||
// The user is logged in.
|
||||
// Fetch the client_id URL to find information about the client to present to the user.
|
||||
try {
|
||||
/** @var ResponseInterface $clientIdResponse */
|
||||
list($clientIdResponse, $clientIdEffectiveUrl) = call_user_func($this->httpGetWithEffectiveUrl, $queryParams['client_id']);
|
||||
$clientIdMf2 = Mf2\parse($clientIdResponse->getBody()->getContents(), $clientIdEffectiveUrl);
|
||||
} catch (ClientExceptionInterface | RequestExceptionInterface | NetworkExceptionInterface $e) {
|
||||
$this->logger->error("Caught an HTTP exception while trying to fetch the client_id. Returning an error response.", [
|
||||
'client_id' => $queryParams['client_id'],
|
||||
'exception' => $e->__toString()
|
||||
]);
|
||||
|
||||
return new Response(500, ['content-type' => 'text/plain'], 'An internal error occurred.');
|
||||
} catch (Exception $e) {
|
||||
$this->logger->error("Caught an unknown exception while trying to fetch the client_id. Returning an error response.", [
|
||||
'exception' => $e->__toString()
|
||||
]);
|
||||
|
||||
return new Response(500, ['content-type' => 'text/plain'], 'An internal error occurred.');
|
||||
}
|
||||
|
||||
// Search for an h-app with u-url matching the client_id.
|
||||
$clientHApps = M\findMicroformatsByProperty(M\findMicroformatsByType($clientIdMf2, 'h-app'), 'url', $queryParams['client-id']);
|
||||
$clientHApp = empty($clientHApps) ? null : $clientHApps[0];
|
||||
|
||||
// Search for all link@rel=redirect_uri at the client_id.
|
||||
$clientIdRedirectUris = [];
|
||||
if (array_key_exists('redirect_uri', $clientIdMf2['rels'])) {
|
||||
$clientIdRedirectUris = array_merge($clientIdRedirectUris, $clientIdMf2['rels']);
|
||||
}
|
||||
foreach (HeaderParser::parse($clientIdResponse->getHeader('Link')) as $link) {
|
||||
if (array_key_exists('rel', $link) and str_contains(" {$link['rel']} ", " redirect_uri ")) {
|
||||
// Strip off the < > which surround the link URL for some reason.
|
||||
$clientIdRedirectUris[] = substr($link[0], 1, strlen($link[0]) - 2);
|
||||
}
|
||||
}
|
||||
|
||||
// If the authority of the redirect_uri does not match the client_id, or exactly match one of their redirect URLs, return an error.
|
||||
$cidComponents = M\parseUrl($queryParams['client_id']);
|
||||
$ruriComponents = M\parseUrl($queryParams['redirect_uri']);
|
||||
$clientIdMatchesRedirectUri = $cidComponents['scheme'] == $ruriComponents['scheme']
|
||||
&& $cidComponents['host'] == $ruriComponents['host']
|
||||
&& $cidComponents['port'] == $ruriComponents['port'];
|
||||
$redirectUriValid = $clientIdMatchesRedirectUri || in_array($queryParams['redirect_uri'], $clientIdRedirectUris);
|
||||
|
||||
if (!$redirectUriValid) {
|
||||
$this->logger->warning("The provided redirect_uri did not match either the client_id, nor the discovered redirect URIs.", [
|
||||
'provided_redirect_uri' => $queryParams['redirect_uri'],
|
||||
'provided_client_id' => $queryParams['client_id'],
|
||||
'discovered_redirect_uris' => $clientIdRedirectUris
|
||||
]);
|
||||
|
||||
return new Response(500, ['content-type' => 'text/plain'], 'An internal error occurred.');
|
||||
}
|
||||
|
||||
// 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 = [
|
||||
|
||||
];
|
||||
$code = array_merge($authenticationResult, [
|
||||
'client_id' => $queryParams['client_id'],
|
||||
'redirect_uri' => $queryParams['redirect_uri'],
|
||||
'state' => $queryParams['state'],
|
||||
'code_challenge' => $queryParams['code_challenge'],
|
||||
'code_challenge_method' => $queryParams['code_challenge_method'],
|
||||
'requested_scope' => $queryParams['scope'] ?? '',
|
||||
'code' => generateRandomString(256)
|
||||
]);
|
||||
|
||||
// Pass it to the auth code customisation callback, if any.
|
||||
$code = call_user_func($this->callbacks[self::CUSTOMIZE_AUTHORIZATION_CODE], $code, $request);
|
||||
@ -164,16 +328,16 @@ class Server {
|
||||
$this->authorizationCodeStorage->put($code['code'], $code);
|
||||
|
||||
// Return a redirect to the client app.
|
||||
return new Response(302, ['Location' => appendQueryParams($queryParams['redirect_uri'], [
|
||||
'code' => $code['code'],
|
||||
'state' => $code['state']
|
||||
])]);
|
||||
}
|
||||
|
||||
// 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);
|
||||
return call_user_func($this->callbacks[self::SHOW_AUTHORIZATION_PAGE], $request, $authenticationResult, $authenticationRedirect, $clientHApp);
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user