diff --git a/composer.json b/composer.json index a779444..aa23462 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ "require": { "psr/http-message": "^1.0", "psr/log": "^1.1", - "nyholm/psr7": "^1.4", + "nyholm/psr7": "1.4.x-dev", "indieauth/client": "^1.1", "psr/http-client": "^1.0", "psr/http-server-middleware": "^1.0", diff --git a/composer.lock b/composer.lock index 83596b2..8592376 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "eb1e0bd9a4a33192017f5ae898884bfc", + "content-hash": "b0410d716bb0a72d683dd504ec19644d", "packages": [ { "name": "barnabywalters/mf-cleaner", @@ -400,16 +400,16 @@ }, { "name": "nyholm/psr7", - "version": "1.4.0", + "version": "dev-master", "source": { "type": "git", "url": "https://github.com/Nyholm/psr7.git", - "reference": "23ae1f00fbc6a886cbe3062ca682391b9cc7c37b" + "reference": "a4a362944244ed20a6bbbecacc882abc8045585a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Nyholm/psr7/zipball/23ae1f00fbc6a886cbe3062ca682391b9cc7c37b", - "reference": "23ae1f00fbc6a886cbe3062ca682391b9cc7c37b", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/a4a362944244ed20a6bbbecacc882abc8045585a", + "reference": "a4a362944244ed20a6bbbecacc882abc8045585a", "shasum": "" }, "require": { @@ -423,11 +423,12 @@ "psr/http-message-implementation": "1.0" }, "require-dev": { - "http-interop/http-factory-tests": "^0.8", + "http-interop/http-factory-tests": "^0.9", "php-http/psr7-integration-tests": "^1.0", "phpunit/phpunit": "^7.5 || 8.5 || 9.4", "symfony/error-handler": "^4.4" }, + "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -461,7 +462,7 @@ ], "support": { "issues": "https://github.com/Nyholm/psr7/issues", - "source": "https://github.com/Nyholm/psr7/tree/1.4.0" + "source": "https://github.com/Nyholm/psr7/tree/master" }, "funding": [ { @@ -473,7 +474,7 @@ "type": "github" } ], - "time": "2021-02-18T15:41:32+00:00" + "time": "2021-05-10T20:13:32+00:00" }, { "name": "p3k/http", @@ -3193,7 +3194,9 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": { + "nyholm/psr7": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": [], diff --git a/src/Server.php b/src/Server.php index 6bcd928..bd7fb4b 100644 --- a/src/Server.php +++ b/src/Server.php @@ -9,8 +9,8 @@ use BarnabyWalters\Mf2 as M; use GuzzleHttp\Psr7\Header as HeaderParser; use Nyholm\Psr7\Response; use PHPUnit\Framework\Constraint\Callback; -use Psr\Http\Client\ClientExceptionInterface; use Psr\Http\Client\ClientInterface as HttpClientInterface; +use Psr\Http\Client\ClientExceptionInterface; use Psr\Http\Client\NetworkExceptionInterface; use Psr\Http\Client\RequestExceptionInterface; use Psr\Http\Message\ResponseInterface; @@ -204,15 +204,14 @@ class Server { // Make a hash of the protected indieauth-specific parameters. $hash = hashAuthorizationRequestParameters($request, $this->secret); $queryParams[self::HASH_QUERY_STRING_KEY] = $hash; - - $authenticationRedirect = $request->getUri()->withQuery(buildQueryString($queryParams)); + $authenticationRedirect = $request->getUri()->withQuery(buildQueryString($queryParams))->__toString(); // User-facing requests always start by calling the authentication request callback. $this->logger->info('Calling handle_authentication_request callback'); - $authenticationResult = call_user_func($this->callbacks[self::HANDLE_AUTHENTICATION_REQUEST], $request, $authenticationRedirect); - + $authenticationResult = call_user_func($this->handleAuthenticationRequestCallback, $request, $authenticationRedirect); + // If the authentication handler returned a Response, return that as-is. - if ($authenticationResult instanceof Response) { + if ($authenticationResult instanceof ResponseInterface) { return $authenticationResult; } elseif (is_array($authenticationResult)) { // Check the resulting array for errors. @@ -225,7 +224,7 @@ class Server { try { /** @var ResponseInterface $clientIdResponse */ list($clientIdResponse, $clientIdEffectiveUrl) = call_user_func($this->httpGetWithEffectiveUrl, $queryParams['client_id']); - $clientIdMf2 = Mf2\parse($clientIdResponse->getBody()->getContents(), $clientIdEffectiveUrl); + $clientIdMf2 = Mf2\parse((string) $clientIdResponse->getBody(), $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'], @@ -306,7 +305,7 @@ class Server { // Pass it to the auth code customisation callback, if any. - $code = call_user_func($this->callbacks[self::HANDLE_AUTHORIZATION_FORM], $request, $code); + $code = $this->authorizationForm->transformAuthorizationCode($request, $code); // Store the authorization code. $this->authorizationCodeStorage->put($code['code'], $code); @@ -320,7 +319,7 @@ class Server { // Otherwise, the user is authenticated and needs to authorize the client app + choose scopes. // Present the authorization UI. - return call_user_func($this->callbacks[self::SHOW_AUTHORIZATION_FORM], $request, $authenticationResult, $authenticationRedirect, $clientHApp); + return $this->authorizationForm->showForm($request, $authenticationResult, $authenticationRedirect, $clientHApp); } } diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 659a30f..2fd718a 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -2,8 +2,10 @@ namespace Taproot\IndieAuth\Test; +use Nyholm\Psr7\Response; use Nyholm\Psr7\ServerRequest; use PHPUnit\Framework\TestCase; +use Psr\Http\Message\ServerRequestInterface; use Taproot\IndieAuth\Callback\SingleUserPasswordAuthenticationCallback; use Taproot\IndieAuth\Server; use Taproot\IndieAuth\Storage\FilesystemJsonStorage; @@ -14,13 +16,24 @@ const ACCESS_TOKEN_STORAGE_PATH = __DIR__ . '/tmp/authorization_codes'; const TMP_DIR = __DIR__ . '/tmp'; class ServerTest extends TestCase { - protected function getDefaultServer() { - return new Server([ + protected function getDefaultServer(array $config=[]) { + return new Server(array_merge([ 'secret' => SERVER_SECRET, 'authorizationCodeStorage' => AUTH_CODE_STORAGE_PATH, 'accessTokenStorage' => ACCESS_TOKEN_STORAGE_PATH, Server::HANDLE_AUTHENTICATION_REQUEST => new SingleUserPasswordAuthenticationCallback(['me' => 'https://example.com/'], password_hash('password', PASSWORD_DEFAULT)) - ]); + ], $config)); + } + + protected function getIARequest(array $params=[]) { + return (new ServerRequest('GET', 'https://example.com/'))->withQueryParams(array_merge([ + 'response_type' => 'code', + 'client_id' => 'https://app.example.com/', + 'redirect_uri' => 'https://app.example.com/indieauth', + 'state' => '12345', + 'code_challenge' => hash('sha256', 'code'), + 'code_challenge_method' => 'sha256' + ], $params)); } protected function setUp(): void { @@ -45,5 +58,52 @@ class ServerTest extends TestCase { $req = (new ServerRequest('GET', 'https://example.com/')); $res = $s->handleAuthorizationEndpointRequest($req); $this->assertEquals(400, $res->getStatusCode()); - } + } + + public function testUnauthenticatedRequestReturnsAuthenticationResponse() { + $expectedResponse = 'You need to authenticate before continuing!'; + $s = $this->getDefaultServer([ + Server::HANDLE_AUTHENTICATION_REQUEST => function (ServerRequestInterface $request, string $formAction) use ($expectedResponse) { + return new Response(200, ['content-type' => 'text/plain'], $expectedResponse); + } + ]); + + $res = $s->handleAuthorizationEndpointRequest($this->getIARequest()); + + $this->assertEquals(200, $res->getStatusCode()); + $this->assertEquals($expectedResponse, (string) $res->getBody()); + } + + public function testReturnsServerErrorIfAuthenticationResultHasNoMeKey() { + $s = $this->getDefaultServer([ + Server::HANDLE_AUTHENTICATION_REQUEST => function (ServerRequestInterface $request, string $formAction) { + return []; + } + ]); + + $res = $s->handleAuthorizationEndpointRequest($this->getIARequest()); + + $this->assertEquals(500, $res->getStatusCode()); + } + + public function testReturnServerErrorIfFetchingClientIdThrowsException() { + $exceptionClasses = ['GuzzleHttp\Exception\ConnectException', 'GuzzleHttp\Exception\RequestException']; + foreach ($exceptionClasses as $eClass) { + $req = $this->getIARequest(); + $s = $this->getDefaultServer([ + Server::HANDLE_AUTHENTICATION_REQUEST => function (ServerRequestInterface $request, string $formAction) { + return ['me' => 'https://example.com/']; + }, + 'httpGetWithEffectiveUrl' => function ($url) use ($eClass, $req) { + throw new $eClass($eClass, $req); + } + ]); + + $res = $s->handleAuthorizationEndpointRequest($req); + + $this->assertEquals(500, $res->getStatusCode()); + } + } + + }