diff --git a/docs/coverage/Callback/AuthorizationFormInterface.php.html b/docs/coverage/Callback/AuthorizationFormInterface.php.html index c83981e..cd375c0 100644 --- a/docs/coverage/Callback/AuthorizationFormInterface.php.html +++ b/docs/coverage/Callback/AuthorizationFormInterface.php.html @@ -163,7 +163,7 @@

Legend

ExecutedNot ExecutedDead Code

- Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Fri Jun 18 14:09:49 UTC 2021. + Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Fri Jun 18 14:43:22 UTC 2021.

diff --git a/docs/coverage/Callback/DefaultAuthorizationForm.php.html b/docs/coverage/Callback/DefaultAuthorizationForm.php.html index c14bd48..3283b2e 100644 --- a/docs/coverage/Callback/DefaultAuthorizationForm.php.html +++ b/docs/coverage/Callback/DefaultAuthorizationForm.php.html @@ -279,7 +279,7 @@ 87 88    public function transformAuthorizationCode(ServerRequestInterface $request, array $code): array { 89        // Add any granted scopes from the form to the code. - 90        $grantedScopes = $request->getParsedBody()['taproot_indieauth_server_scope[]'] ?? []; + 90        $grantedScopes = $request->getParsedBody()['taproot_indieauth_server_scope'] ?? []; 91 92        // This default implementation naievely accepts any scopes it receives from the form. 93        // You may wish to perform some sort of validation. @@ -305,7 +305,7 @@

Legend

ExecutedNot ExecutedDead Code

- Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Fri Jun 18 14:09:49 UTC 2021. + Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Fri Jun 18 14:43:22 UTC 2021.

diff --git a/docs/coverage/Callback/SingleUserPasswordAuthenticationCallback.php.html b/docs/coverage/Callback/SingleUserPasswordAuthenticationCallback.php.html index 1fd6f55..f626ddf 100644 --- a/docs/coverage/Callback/SingleUserPasswordAuthenticationCallback.php.html +++ b/docs/coverage/Callback/SingleUserPasswordAuthenticationCallback.php.html @@ -101,7 +101,7 @@ -  __construct +  __construct
100.00% covered (success) @@ -122,7 +122,7 @@ -  __invoke +  __invoke
100.00% covered (success) @@ -184,92 +184,93 @@ 34 * $server = new IndieAuth\Server([ 35 *   … 36 *   'authenticationHandler' => new IndieAuth\Callback\SingleUserPasswordAuthenticationCallback( - 37 *     ['me' => 'https://me.example.com/'], - 38 *     YOUR_HASHED_PASSWORD - 39 *   ) - 40 *   … - 41 * ]); - 42 * ``` - 43 *  - 44 * See documentation for `__construct()` for information about customising behaviour. - 45 */ - 46class SingleUserPasswordAuthenticationCallback { - 47    const PASSWORD_FORM_PARAMETER = 'taproot_indieauth_server_password'; - 48    const LOGIN_HASH_COOKIE = 'taproot_indieauth_server_supauth_hash'; - 49    const DEFAULT_COOKIE_TTL = 60 * 5; - 50 - 51    public string $csrfKey; - 52    public string $formTemplate; - 53    protected array $user; - 54    protected string $hashedPassword; - 55    protected string $secret; - 56    protected int $ttl; - 57     - 58    /** - 59     * Constructor - 60     *  - 61     * @param string $secret A secret key used to encrypt cookies. Can be the same as the secret passed to IndieAuth\Server. - 62     * @param array $user An array representing the user, which will be returned on a successful authentication. MUST include a 'me' key, may also contain a 'profile' key, or other keys at your discretion. - 63     * @param string $hashedPassword The password used to authenticate as $user, hashed by `password_hash($pass, PASSWORD_DEFAULT)` - 64     * @param string|null $formTemplate The path to a template used to render the sign-in form. Uses default if null. - 65     * @param string|null $csrfKey The key under which to fetch a CSRF token from `$request` attributes, and as the CSRF token name in submitted form data. Defaults to the Server default, only change if you’re using a custom CSRF middleware. - 66     * @param int|null $ttl The lifetime of the authentication cookie. Defaults to five minutes. - 67     */ - 68    public function __construct(string $secret, array $user, string $hashedPassword, ?string $formTemplate=null, ?string $csrfKey=null, ?int $ttl=self::DEFAULT_COOKIE_TTL) { - 69        if (strlen($secret) < 64) { - 70            throw new BadMethodCallException("\$secret must be a string with a minimum length of 64 characters."); - 71        } - 72        $this->secret = $secret; - 73 - 74        $this->ttl = $ttl; - 75 - 76        if (!isset($user['me'])) { - 77            throw new BadMethodCallException('The $user array MUST contain a “me” key, the value which must be the user’s canonical URL as a string.'); - 78        } - 79         - 80        if (is_null(password_get_info($hashedPassword)['algo'])) { - 81            throw new BadMethodCallException('The provided $hashedPassword was not a valid hash created by the password_hash() function.'); - 82        } - 83        $this->user = $user; - 84        $this->hashedPassword = $hashedPassword; - 85        $this->formTemplate = $formTemplate ?? __DIR__ . '/../../templates/single_user_password_authentication_form.html.php'; - 86        $this->csrfKey = $csrfKey ?? \Taproot\IndieAuth\Server::DEFAULT_CSRF_KEY; - 87    } - 88 - 89    public function __invoke(ServerRequestInterface $request, string $formAction, ?string $normalizedMeUrl=null) { - 90        // If the request is logged in, return authentication data. - 91        $cookies = $request->getCookieParams(); - 92        if ( - 93            isset($cookies[self::LOGIN_HASH_COOKIE]) - 94            && hash_equals(hash_hmac('SHA256', json_encode($this->user), $this->secret), $cookies[self::LOGIN_HASH_COOKIE]) - 95        ) { - 96            return $this->user; - 97        } - 98 - 99        // If the request is a form submission with a matching password, return a redirect to the indieauth - 100        // flow, setting a cookie. - 101        if ($request->getMethod() == 'POST' && password_verify($request->getParsedBody()[self::PASSWORD_FORM_PARAMETER] ?? '', $this->hashedPassword)) { - 102            $response = new Response(302, ['Location' => $formAction]); - 103 - 104            // Set the user data hash cookie. - 105            $response = FigCookies\FigResponseCookies::set($response, FigCookies\SetCookie::create(self::LOGIN_HASH_COOKIE) - 106                    ->withValue(hash_hmac('SHA256', json_encode($this->user), $this->secret)) - 107                    ->withMaxAge($this->ttl) - 108                    ->withSecure($request->getUri()->getScheme() == 'https') - 109                    ->withDomain($request->getUri()->getHost()) - 110            ); - 111 - 112            return $response; - 113        } - 114 - 115        // Otherwise, return a response containing the password form. - 116        return new Response(200, ['content-type' => 'text/html'], renderTemplate($this->formTemplate, [ - 117            'formAction' => $formAction, - 118            'request' => $request, - 119            'csrfFormElement' => '<input type="hidden" name="' . htmlentities($this->csrfKey) . '" value="' . htmlentities($request->getAttribute($this->csrfKey)) . '" />' - 120        ])); - 121    } - 122} + 37 *     YOUR_SECRET, + 38 *     ['me' => 'https://me.example.com/'], + 39 *     YOUR_HASHED_PASSWORD + 40 *   ) + 41 *   … + 42 * ]); + 43 * ``` + 44 *  + 45 * See documentation for `__construct()` for information about customising behaviour. + 46 */ + 47class SingleUserPasswordAuthenticationCallback { + 48    const PASSWORD_FORM_PARAMETER = 'taproot_indieauth_server_password'; + 49    const LOGIN_HASH_COOKIE = 'taproot_indieauth_server_supauth_hash'; + 50    const DEFAULT_COOKIE_TTL = 60 * 5; + 51 + 52    public string $csrfKey; + 53    public string $formTemplate; + 54    protected array $user; + 55    protected string $hashedPassword; + 56    protected string $secret; + 57    protected int $ttl; + 58     + 59    /** + 60     * Constructor + 61     *  + 62     * @param string $secret A secret key used to encrypt cookies. Can be the same as the secret passed to IndieAuth\Server. + 63     * @param array $user An array representing the user, which will be returned on a successful authentication. MUST include a 'me' key, may also contain a 'profile' key, or other keys at your discretion. + 64     * @param string $hashedPassword The password used to authenticate as $user, hashed by `password_hash($pass, PASSWORD_DEFAULT)` + 65     * @param string|null $formTemplate The path to a template used to render the sign-in form. Uses default if null. + 66     * @param string|null $csrfKey The key under which to fetch a CSRF token from `$request` attributes, and as the CSRF token name in submitted form data. Defaults to the Server default, only change if you’re using a custom CSRF middleware. + 67     * @param int|null $ttl The lifetime of the authentication cookie, in seconds. Defaults to five minutes. + 68     */ + 69    public function __construct(string $secret, array $user, string $hashedPassword, ?string $formTemplate=null, ?string $csrfKey=null, ?int $ttl=null) { + 70        if (strlen($secret) < 64) { + 71            throw new BadMethodCallException("\$secret must be a string with a minimum length of 64 characters."); + 72        } + 73        $this->secret = $secret; + 74 + 75        $this->ttl = $ttl ?? self::DEFAULT_COOKIE_TTL; + 76 + 77        if (!isset($user['me'])) { + 78            throw new BadMethodCallException('The $user array MUST contain a “me” key, the value which must be the user’s canonical URL as a string.'); + 79        } + 80         + 81        if (is_null(password_get_info($hashedPassword)['algo'])) { + 82            throw new BadMethodCallException('The provided $hashedPassword was not a valid hash created by the password_hash() function.'); + 83        } + 84        $this->user = $user; + 85        $this->hashedPassword = $hashedPassword; + 86        $this->formTemplate = $formTemplate ?? __DIR__ . '/../../templates/single_user_password_authentication_form.html.php'; + 87        $this->csrfKey = $csrfKey ?? \Taproot\IndieAuth\Server::DEFAULT_CSRF_KEY; + 88    } + 89 + 90    public function __invoke(ServerRequestInterface $request, string $formAction, ?string $normalizedMeUrl=null) { + 91        // If the request is logged in, return authentication data. + 92        $cookies = $request->getCookieParams(); + 93        if ( + 94            isset($cookies[self::LOGIN_HASH_COOKIE]) + 95            && hash_equals(hash_hmac('SHA256', json_encode($this->user), $this->secret), $cookies[self::LOGIN_HASH_COOKIE]) + 96        ) { + 97            return $this->user; + 98        } + 99 + 100        // If the request is a form submission with a matching password, return a redirect to the indieauth + 101        // flow, setting a cookie. + 102        if ($request->getMethod() == 'POST' && password_verify($request->getParsedBody()[self::PASSWORD_FORM_PARAMETER] ?? '', $this->hashedPassword)) { + 103            $response = new Response(302, ['Location' => $formAction]); + 104 + 105            // Set the user data hash cookie. + 106            $response = FigCookies\FigResponseCookies::set($response, FigCookies\SetCookie::create(self::LOGIN_HASH_COOKIE) + 107                    ->withValue(hash_hmac('SHA256', json_encode($this->user), $this->secret)) + 108                    ->withMaxAge($this->ttl) + 109                    ->withSecure($request->getUri()->getScheme() == 'https') + 110                    ->withDomain($request->getUri()->getHost()) + 111            ); + 112 + 113            return $response; + 114        } + 115 + 116        // Otherwise, return a response containing the password form. + 117        return new Response(200, ['content-type' => 'text/html'], renderTemplate($this->formTemplate, [ + 118            'formAction' => $formAction, + 119            'request' => $request, + 120            'csrfFormElement' => '<input type="hidden" name="' . htmlentities($this->csrfKey) . '" value="' . htmlentities($request->getAttribute($this->csrfKey)) . '" />' + 121        ])); + 122    } + 123} @@ -280,7 +281,7 @@

Legend

ExecutedNot ExecutedDead Code

- Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Fri Jun 18 14:09:49 UTC 2021. + Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Fri Jun 18 14:43:22 UTC 2021.

diff --git a/docs/coverage/Callback/dashboard.html b/docs/coverage/Callback/dashboard.html index 716d0cd..50fddc6 100644 --- a/docs/coverage/Callback/dashboard.html +++ b/docs/coverage/Callback/dashboard.html @@ -136,7 +136,7 @@
@@ -223,7 +223,7 @@ $(document).ready(function() { chart.yAxis.axisLabel('Cyclomatic Complexity'); d3.select('#classComplexity svg') - .datum(getComplexityData([[100,6,"Taproot\\IndieAuth\\Callback\\DefaultAuthorizationForm<\/a>"],[100,9,"Taproot\\IndieAuth\\Callback\\SingleUserPasswordAuthenticationCallback<\/a>"]], 'Class Complexity')) + .datum(getComplexityData([[100,6,"Taproot\\IndieAuth\\Callback\\DefaultAuthorizationForm<\/a>"],[100,9,"Taproot\\IndieAuth\\Callback\\SingleUserPasswordAuthenticationCallback<\/a>"]], 'Class Complexity')) .transition() .duration(500) .call(chart); @@ -247,7 +247,7 @@ $(document).ready(function() { chart.yAxis.axisLabel('Method Complexity'); d3.select('#methodComplexity svg') - .datum(getComplexityData([[100,1,"Taproot\\IndieAuth\\Callback\\DefaultAuthorizationForm::__construct<\/a>"],[100,3,"Taproot\\IndieAuth\\Callback\\DefaultAuthorizationForm::showForm<\/a>"],[100,1,"Taproot\\IndieAuth\\Callback\\DefaultAuthorizationForm::transformAuthorizationCode<\/a>"],[100,1,"Taproot\\IndieAuth\\Callback\\DefaultAuthorizationForm::setLogger<\/a>"],[100,4,"Taproot\\IndieAuth\\Callback\\SingleUserPasswordAuthenticationCallback::__construct<\/a>"],[100,5,"Taproot\\IndieAuth\\Callback\\SingleUserPasswordAuthenticationCallback::__invoke<\/a>"]], 'Method Complexity')) + .datum(getComplexityData([[100,1,"Taproot\\IndieAuth\\Callback\\DefaultAuthorizationForm::__construct<\/a>"],[100,3,"Taproot\\IndieAuth\\Callback\\DefaultAuthorizationForm::showForm<\/a>"],[100,1,"Taproot\\IndieAuth\\Callback\\DefaultAuthorizationForm::transformAuthorizationCode<\/a>"],[100,1,"Taproot\\IndieAuth\\Callback\\DefaultAuthorizationForm::setLogger<\/a>"],[100,4,"Taproot\\IndieAuth\\Callback\\SingleUserPasswordAuthenticationCallback::__construct<\/a>"],[100,5,"Taproot\\IndieAuth\\Callback\\SingleUserPasswordAuthenticationCallback::__invoke<\/a>"]], 'Method Complexity')) .transition() .duration(500) .call(chart); diff --git a/docs/coverage/Callback/index.html b/docs/coverage/Callback/index.html index c80d6eb..f453d9a 100644 --- a/docs/coverage/Callback/index.html +++ b/docs/coverage/Callback/index.html @@ -152,7 +152,7 @@ High: 90% to 100%

- Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Fri Jun 18 14:09:49 UTC 2021. + Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Fri Jun 18 14:43:22 UTC 2021.

diff --git a/docs/coverage/IndieAuthException.php.html b/docs/coverage/IndieAuthException.php.html index 1d2873e..c5fb09e 100644 --- a/docs/coverage/IndieAuthException.php.html +++ b/docs/coverage/IndieAuthException.php.html @@ -326,7 +326,7 @@

Legend

ExecutedNot ExecutedDead Code

- Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Fri Jun 18 14:09:49 UTC 2021. + Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Fri Jun 18 14:43:22 UTC 2021.

diff --git a/docs/coverage/Middleware/ClosureRequestHandler.php.html b/docs/coverage/Middleware/ClosureRequestHandler.php.html index 9becc60..1da4b9b 100644 --- a/docs/coverage/Middleware/ClosureRequestHandler.php.html +++ b/docs/coverage/Middleware/ClosureRequestHandler.php.html @@ -180,7 +180,7 @@

Legend

ExecutedNot ExecutedDead Code

- Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Fri Jun 18 14:09:49 UTC 2021. + Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Fri Jun 18 14:43:22 UTC 2021.

diff --git a/docs/coverage/Middleware/DoubleSubmitCookieCsrfMiddleware.php.html b/docs/coverage/Middleware/DoubleSubmitCookieCsrfMiddleware.php.html index ffef89b..4cc1561 100644 --- a/docs/coverage/Middleware/DoubleSubmitCookieCsrfMiddleware.php.html +++ b/docs/coverage/Middleware/DoubleSubmitCookieCsrfMiddleware.php.html @@ -324,7 +324,7 @@

Legend

ExecutedNot ExecutedDead Code

- Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Fri Jun 18 14:09:49 UTC 2021. + Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Fri Jun 18 14:43:22 UTC 2021.

diff --git a/docs/coverage/Middleware/NoOpMiddleware.php.html b/docs/coverage/Middleware/NoOpMiddleware.php.html index 2e11a6b..5e67154 100644 --- a/docs/coverage/Middleware/NoOpMiddleware.php.html +++ b/docs/coverage/Middleware/NoOpMiddleware.php.html @@ -157,7 +157,7 @@

Legend

ExecutedNot ExecutedDead Code

- Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Fri Jun 18 14:09:49 UTC 2021. + Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Fri Jun 18 14:43:22 UTC 2021.

diff --git a/docs/coverage/Middleware/ResponseRequestHandler.php.html b/docs/coverage/Middleware/ResponseRequestHandler.php.html index 25849cb..d4e1fee 100644 --- a/docs/coverage/Middleware/ResponseRequestHandler.php.html +++ b/docs/coverage/Middleware/ResponseRequestHandler.php.html @@ -177,7 +177,7 @@

Legend

ExecutedNot ExecutedDead Code

- Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Fri Jun 18 14:09:49 UTC 2021. + Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Fri Jun 18 14:43:22 UTC 2021.

diff --git a/docs/coverage/Middleware/dashboard.html b/docs/coverage/Middleware/dashboard.html index a0b8511..c453628 100644 --- a/docs/coverage/Middleware/dashboard.html +++ b/docs/coverage/Middleware/dashboard.html @@ -138,7 +138,7 @@
diff --git a/docs/coverage/Middleware/index.html b/docs/coverage/Middleware/index.html index 7627520..f043cba 100644 --- a/docs/coverage/Middleware/index.html +++ b/docs/coverage/Middleware/index.html @@ -195,7 +195,7 @@ High: 90% to 100%

- Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Fri Jun 18 14:09:49 UTC 2021. + Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Fri Jun 18 14:43:22 UTC 2021.

diff --git a/docs/coverage/Server.php.html b/docs/coverage/Server.php.html index d422b61..0490f7c 100644 --- a/docs/coverage/Server.php.html +++ b/docs/coverage/Server.php.html @@ -61,13 +61,13 @@
4 / 5
CRAP
-
- 96.69% covered (success) +
+ 96.70% covered (success)
-
96.69%
-
263 / 272
+
96.70%
+
264 / 273
@@ -90,13 +90,13 @@
4 / 5
105
-
- 96.69% covered (success) +
+ 96.70% covered (success)
-
96.69%
-
263 / 272
+
96.70%
+
264 / 273
@@ -151,19 +151,19 @@
0.00%
0 / 1
- 67.70 + 67.69
-
- 94.61% covered (success) +
+ 94.64% covered (success)
-
94.61%
-
158 / 167
+
94.64%
+
159 / 168
-  handleTokenEndpointRequest +  handleTokenEndpointRequest
100.00% covered (success) @@ -184,7 +184,7 @@ -  handleException +  handleException
100.00% covered (success) @@ -829,237 +829,238 @@ 617 618                            // Pass it to the auth code customisation callback. 619                            $code = $this->authorizationForm->transformAuthorizationCode($request, $code); - 620 - 621                            // Store the authorization code. - 622                            $authCode = $this->tokenStorage->createAuthCode($code); - 623                            if (is_null($authCode)) { - 624                                // If saving the authorization code failed silently, there isn’t much we can do about it, - 625                                // but should at least log and return an error. - 626                                $this->logger->error("Saving the authorization code failed and returned false without raising an exception."); - 627                                throw IndieAuthException::create(IndieAuthException::INTERNAL_ERROR_REDIRECT, $request); - 628                            } - 629                             - 630                            // Return a redirect to the client app. - 631                            return new Response(302, [ - 632                                'Location' => appendQueryParams($queryParams['redirect_uri'], [ - 633                                    'code' => $authCode, - 634                                    'state' => $code['state'] - 635                                ]), - 636                                'Cache-control' => 'no-cache' - 637                            ]); - 638                        } - 639 - 640                        // Otherwise, the user is authenticated and needs to authorize the client app + choose scopes. - 641 - 642                        // Fetch the client_id URL to find information about the client to present to the user. - 643                        // TODO: in order to comply with https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1, - 644                        // it may be necessary to do this before returning any other kind of error response, as, per - 645                        // the spec, errors should only be shown to the user if the client_id and redirect_uri parameters - 646                        // are missing or invalid. Otherwise, they should be sent back to the client with an error - 647                        // redirect response. - 648                        if (is_null($clientIdResponse) || is_null($clientIdEffectiveUrl) || is_null($clientIdMf2)) { - 649                            try { - 650                                /** @var ResponseInterface $clientIdResponse */ - 651                                list($clientIdResponse, $clientIdEffectiveUrl) = call_user_func($this->httpGetWithEffectiveUrl, $queryParams['client_id']); - 652                                $clientIdMf2 = Mf2\parse((string) $clientIdResponse->getBody(), $clientIdEffectiveUrl); - 653                            } catch (ClientExceptionInterface | RequestExceptionInterface | NetworkExceptionInterface $e) { - 654                                $this->logger->error("Caught an HTTP exception while trying to fetch the client_id. Returning an error response.", [ - 655                                    'client_id' => $queryParams['client_id'], - 656                                    'exception' => $e->__toString() - 657                                ]); - 658                                 - 659                                // At this point in the flow, we’ve already guaranteed that the redirect_uri is valid, - 660                                // so in theory we should report these errors by redirecting there. - 661                                throw IndieAuthException::create(IndieAuthException::INTERNAL_ERROR_REDIRECT, $request, $e); - 662                            } catch (Exception $e) { - 663                                $this->logger->error("Caught an unknown exception while trying to fetch the client_id. Returning an error response.", [ - 664                                    'exception' => $e->__toString() - 665                                ]); - 666     - 667                                throw IndieAuthException::create(IndieAuthException::INTERNAL_ERROR_REDIRECT, $request, $e); - 668                            } - 669                        } - 670                         - 671                        // Search for an h-app with u-url matching the client_id. - 672                        $clientHApps = M\findMicroformatsByProperty(M\findMicroformatsByType($clientIdMf2, 'h-app'), 'url', $queryParams['client_id']); - 673                        $clientHApp = empty($clientHApps) ? null : $clientHApps[0]; - 674 - 675                        // Present the authorization UI. - 676                        return $this->authorizationForm->showForm($request, $authenticationResult, $authenticationRedirect, $clientHApp) - 677                                ->withAddedHeader('Cache-control', 'no-cache'); - 678                    } - 679                } - 680 - 681                // If the request isn’t an IndieAuth Authorization or Code-redeeming request, it’s either an invalid - 682                // request or something to do with a custom auth handler (e.g. sending a one-time code in an email.) - 683                $nonIndieAuthRequestResult = call_user_func($this->handleNonIndieAuthRequest, $request); - 684                if ($nonIndieAuthRequestResult instanceof ResponseInterface) { - 685                    return $nonIndieAuthRequestResult; - 686                } else { - 687                    // In this code path we have not validated the redirect_uri, so show a regular error page - 688                    // rather than returning a redirect error. - 689                    throw IndieAuthException::create(IndieAuthException::INTERNAL_ERROR, $request); - 690                } - 691            } catch (IndieAuthException $e) { - 692                // All IndieAuthExceptions will already have been logged. - 693                return $this->handleException($e); - 694            } catch (Exception $e) { - 695                // Unknown exceptions will not have been logged; do so now. - 696                $this->logger->error("Caught unknown exception: {$e}"); - 697                return $this->handleException(IndieAuthException::create(0, $request, $e)); - 698            } - 699        }));     - 700    } - 701 - 702    /** - 703     * Handle Token Endpoint Request - 704     *  - 705     * Handles requests to the IndieAuth token endpoint. The logical flow can be summarised as follows: - 706     *  - 707     * * Check that the request is a code redeeming request. Return an error if not. - 708     * * Ensure that all required parameters are present. Return an error if not. - 709     * * Attempt to exchange the `code` parameter for an access token. Return an error if it fails. - 710     * * Make sure the client_id and redirect_uri request parameters match those stored in the auth code. If not, revoke the access token and return an error. - 711     * * Make sure the provided code_verifier hashes to the code_challenge stored in the auth code. If not, revoke the access token and return an error. - 712     * * Make sure the granted scope stored in the auth code is not empty. If it is, revoke the access token and return an error. - 713     * * Otherwise, return a success response containing information about the issued access token. - 714     *  - 715     * This method must NOT be CSRF-protected as it accepts external requests from client apps. - 716     *  - 717     * @param ServerRequestInterface $request - 718     * @return ResponseInterface - 719     */ - 720    public function handleTokenEndpointRequest(ServerRequestInterface $request): ResponseInterface { - 721        if (isIndieAuthAuthorizationCodeRedeemingRequest($request)) { - 722            $this->logger->info('Handling a request to redeem an authorization code for profile information.'); - 723             - 724            $bodyParams = $request->getParsedBody(); - 725 - 726            if (!isset($bodyParams['code'])) { - 727                $this->logger->warning('The exchange request was missing the code parameter. Returning an error response.'); - 728                return new Response(400, ['content-type' => 'application/json'], json_encode([ - 729                    'error' => 'invalid_request', - 730                    'error_description' => 'The code parameter was missing.' - 731                ])); - 732            } - 733 - 734            // Attempt to internally exchange the provided auth code for an access token. - 735            // We do this before anything else so that the auth code is invalidated as soon as the request starts, - 736            // and the resulting access token is revoked if we encounter an error. This ends up providing a simpler - 737            // and more flexible interface for TokenStorage implementors. - 738            try { - 739                // Call the token exchange method, passing in a callback which performs additional validation - 740                // on the auth code before it gets exchanged. - 741                $tokenData = $this->tokenStorage->exchangeAuthCodeForAccessToken($bodyParams['code'], function (array $authCode) use ($request, $bodyParams) { - 742                    // Verify that all required parameters are included. - 743                    $requiredParameters = ($this->requirePkce or !empty($authCode['code_challenge'])) ? ['client_id', 'redirect_uri', 'code_verifier'] : ['client_id', 'redirect_uri']; - 744                    $missingRequiredParameters = array_filter($requiredParameters, function ($p) use ($bodyParams) { - 745                        return !array_key_exists($p, $bodyParams) || empty($bodyParams[$p]); - 746                    }); - 747                    if (!empty($missingRequiredParameters)) { - 748                        $this->logger->warning('The exchange request was missing required parameters. Returning an error response.', ['missing' => $missingRequiredParameters]); - 749                        throw IndieAuthException::create(IndieAuthException::INVALID_REQUEST, $request); - 750                    } - 751 - 752                    // Verify that it was issued for the same client_id and redirect_uri - 753                    if ($authCode['client_id'] !== $bodyParams['client_id'] - 754                        || $authCode['redirect_uri'] !== $bodyParams['redirect_uri']) { - 755                        $this->logger->error("The provided client_id and/or redirect_uri did not match those stored in the token."); - 756                        throw IndieAuthException::create(IndieAuthException::INVALID_GRANT, $request); - 757                    } - 758 - 759                    // If the auth code was requested with no code_challenge, but the exchange request provides a  - 760                    // code_verifier, return an error. - 761                    if (!empty($bodyParams['code_verifier']) && empty($authCode['code_challenge'])) { - 762                        $this->logger->error("A code_verifier was provided when trying to exchange an auth code requested without a code_challenge."); - 763                        throw IndieAuthException::create(IndieAuthException::INVALID_GRANT, $request); - 764                    } - 765 - 766                    if ($this->requirePkce or !empty($authCode['code_challenge'])) { - 767                        // Check that the supplied code_verifier hashes to the stored code_challenge - 768                        // TODO: support method = plain as well as S256. - 769                        if (!hash_equals($authCode['code_challenge'], generatePKCECodeChallenge($bodyParams['code_verifier']))) { - 770                            $this->logger->error("The provided code_verifier did not hash to the stored code_challenge"); - 771                            throw IndieAuthException::create(IndieAuthException::INVALID_GRANT, $request); - 772                        } - 773                    } - 774                     - 775                    // Check that scope is not empty. - 776                    if (empty($authCode['scope'])) { - 777                        $this->logger->error("An exchange request for a token with an empty scope was sent to the token endpoint."); - 778                        throw IndieAuthException::create(IndieAuthException::INVALID_GRANT, $request); - 779                    } - 780                }); - 781            } catch (IndieAuthException $e) { - 782                // If an exception was thrown, return a corresponding error response. - 783                return new Response(400, ['content-type' => 'application/json'], json_encode([ - 784                    'error' => $e->getInfo()['error'], - 785                    'error_description' => $e->getMessage() - 786                ])); - 787            } - 788 - 789            if (is_null($tokenData)) { - 790                $this->logger->error('Attempting to exchange an auth code for a token resulted in null.', $bodyParams); - 791                return new Response(400, ['content-type' => 'application/json'], json_encode([ - 792                    'error' => 'invalid_grant', - 793                    'error_description' => 'The provided credentials were not valid.' - 794                ])); - 795            } - 796 - 797            // TODO: return an error if the token doesn’t contain a me key. - 798 - 799            // If everything checked out, return {"me": "https://example.com"} response - 800            return new Response(200, [ - 801                'content-type' => 'application/json', - 802                'cache-control' => 'no-store', - 803            ], json_encode(array_merge([ - 804                // Ensure that the token_type key is present, if tokenStorage doesn’t include it. - 805                'token_type' => 'Bearer' - 806            ], array_filter($tokenData, function (string $k) { - 807                // We should be able to trust the return data from tokenStorage, but there’s no harm in - 808                // preventing code_challenges from leaking, per OAuth2. - 809                return !in_array($k, ['code_challenge', 'code_challenge_method']); - 810            }, ARRAY_FILTER_USE_KEY)))); - 811        } - 812 - 813        return new Response(400, ['content-type' => 'application/json'], json_encode([ - 814            'error' => 'invalid_request', - 815            'error_description' => 'Request to token endpoint was not a valid code exchange request.' - 816        ])); - 817    } - 818 - 819    /** - 820     * Handle Exception - 821     *  - 822     * Turns an instance of `IndieAuthException` into an appropriate instance of `ResponseInterface`. - 823     */ - 824    protected function handleException(IndieAuthException $exception): ResponseInterface { - 825        $exceptionData = $exception->getInfo(); - 826 - 827        if ($exceptionData['statusCode'] == 302) { - 828            // This exception is handled by redirecting to the redirect_uri with error parameters. - 829            $redirectQueryParams = [ - 830                'error' => $exceptionData['error'] ?? 'invalid_request', - 831                'error_description' => (string) $exception - 832            ]; - 833 - 834            // If the state parameter was valid, include it in the error redirect. - 835            if ($exception->getCode() !== IndieAuthException::INVALID_STATE) { - 836                $redirectQueryParams['state'] = $exception->getRequest()->getQueryParams()['state']; - 837            } - 838 - 839            return new Response($exceptionData['statusCode'], [ - 840                'Location' => appendQueryParams((string) $exception->getRequest()->getQueryParams()['redirect_uri'], $redirectQueryParams) - 841            ]); - 842        } else { - 843            // This exception should be shown to the user. - 844            return new Response($exception->getStatusCode(), ['content-type' => 'text/html'], renderTemplate($this->exceptionTemplatePath, [ - 845                'request' => $exception->getRequest(), - 846                'exception' => $exception - 847            ])); - 848        } - 849    } - 850} + 620                            $this->logger->info("Creating an authorization code:", ['data' => $code]); + 621 + 622                            // Store the authorization code. + 623                            $authCode = $this->tokenStorage->createAuthCode($code); + 624                            if (is_null($authCode)) { + 625                                // If saving the authorization code failed silently, there isn’t much we can do about it, + 626                                // but should at least log and return an error. + 627                                $this->logger->error("Saving the authorization code failed and returned false without raising an exception."); + 628                                throw IndieAuthException::create(IndieAuthException::INTERNAL_ERROR_REDIRECT, $request); + 629                            } + 630                             + 631                            // Return a redirect to the client app. + 632                            return new Response(302, [ + 633                                'Location' => appendQueryParams($queryParams['redirect_uri'], [ + 634                                    'code' => $authCode, + 635                                    'state' => $code['state'] + 636                                ]), + 637                                'Cache-control' => 'no-cache' + 638                            ]); + 639                        } + 640 + 641                        // Otherwise, the user is authenticated and needs to authorize the client app + choose scopes. + 642 + 643                        // Fetch the client_id URL to find information about the client to present to the user. + 644                        // TODO: in order to comply with https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1, + 645                        // it may be necessary to do this before returning any other kind of error response, as, per + 646                        // the spec, errors should only be shown to the user if the client_id and redirect_uri parameters + 647                        // are missing or invalid. Otherwise, they should be sent back to the client with an error + 648                        // redirect response. + 649                        if (is_null($clientIdResponse) || is_null($clientIdEffectiveUrl) || is_null($clientIdMf2)) { + 650                            try { + 651                                /** @var ResponseInterface $clientIdResponse */ + 652                                list($clientIdResponse, $clientIdEffectiveUrl) = call_user_func($this->httpGetWithEffectiveUrl, $queryParams['client_id']); + 653                                $clientIdMf2 = Mf2\parse((string) $clientIdResponse->getBody(), $clientIdEffectiveUrl); + 654                            } catch (ClientExceptionInterface | RequestExceptionInterface | NetworkExceptionInterface $e) { + 655                                $this->logger->error("Caught an HTTP exception while trying to fetch the client_id. Returning an error response.", [ + 656                                    'client_id' => $queryParams['client_id'], + 657                                    'exception' => $e->__toString() + 658                                ]); + 659                                 + 660                                // At this point in the flow, we’ve already guaranteed that the redirect_uri is valid, + 661                                // so in theory we should report these errors by redirecting there. + 662                                throw IndieAuthException::create(IndieAuthException::INTERNAL_ERROR_REDIRECT, $request, $e); + 663                            } catch (Exception $e) { + 664                                $this->logger->error("Caught an unknown exception while trying to fetch the client_id. Returning an error response.", [ + 665                                    'exception' => $e->__toString() + 666                                ]); + 667     + 668                                throw IndieAuthException::create(IndieAuthException::INTERNAL_ERROR_REDIRECT, $request, $e); + 669                            } + 670                        } + 671                         + 672                        // Search for an h-app with u-url matching the client_id. + 673                        $clientHApps = M\findMicroformatsByProperty(M\findMicroformatsByType($clientIdMf2, 'h-app'), 'url', $queryParams['client_id']); + 674                        $clientHApp = empty($clientHApps) ? null : $clientHApps[0]; + 675 + 676                        // Present the authorization UI. + 677                        return $this->authorizationForm->showForm($request, $authenticationResult, $authenticationRedirect, $clientHApp) + 678                                ->withAddedHeader('Cache-control', 'no-cache'); + 679                    } + 680                } + 681 + 682                // If the request isn’t an IndieAuth Authorization or Code-redeeming request, it’s either an invalid + 683                // request or something to do with a custom auth handler (e.g. sending a one-time code in an email.) + 684                $nonIndieAuthRequestResult = call_user_func($this->handleNonIndieAuthRequest, $request); + 685                if ($nonIndieAuthRequestResult instanceof ResponseInterface) { + 686                    return $nonIndieAuthRequestResult; + 687                } else { + 688                    // In this code path we have not validated the redirect_uri, so show a regular error page + 689                    // rather than returning a redirect error. + 690                    throw IndieAuthException::create(IndieAuthException::INTERNAL_ERROR, $request); + 691                } + 692            } catch (IndieAuthException $e) { + 693                // All IndieAuthExceptions will already have been logged. + 694                return $this->handleException($e); + 695            } catch (Exception $e) { + 696                // Unknown exceptions will not have been logged; do so now. + 697                $this->logger->error("Caught unknown exception: {$e}"); + 698                return $this->handleException(IndieAuthException::create(0, $request, $e)); + 699            } + 700        }));     + 701    } + 702 + 703    /** + 704     * Handle Token Endpoint Request + 705     *  + 706     * Handles requests to the IndieAuth token endpoint. The logical flow can be summarised as follows: + 707     *  + 708     * * Check that the request is a code redeeming request. Return an error if not. + 709     * * Ensure that all required parameters are present. Return an error if not. + 710     * * Attempt to exchange the `code` parameter for an access token. Return an error if it fails. + 711     * * Make sure the client_id and redirect_uri request parameters match those stored in the auth code. If not, revoke the access token and return an error. + 712     * * Make sure the provided code_verifier hashes to the code_challenge stored in the auth code. If not, revoke the access token and return an error. + 713     * * Make sure the granted scope stored in the auth code is not empty. If it is, revoke the access token and return an error. + 714     * * Otherwise, return a success response containing information about the issued access token. + 715     *  + 716     * This method must NOT be CSRF-protected as it accepts external requests from client apps. + 717     *  + 718     * @param ServerRequestInterface $request + 719     * @return ResponseInterface + 720     */ + 721    public function handleTokenEndpointRequest(ServerRequestInterface $request): ResponseInterface { + 722        if (isIndieAuthAuthorizationCodeRedeemingRequest($request)) { + 723            $this->logger->info('Handling a request to redeem an authorization code for an access token.'); + 724             + 725            $bodyParams = $request->getParsedBody(); + 726 + 727            if (!isset($bodyParams['code'])) { + 728                $this->logger->warning('The exchange request was missing the code parameter. Returning an error response.'); + 729                return new Response(400, ['content-type' => 'application/json'], json_encode([ + 730                    'error' => 'invalid_request', + 731                    'error_description' => 'The code parameter was missing.' + 732                ])); + 733            } + 734 + 735            // Attempt to internally exchange the provided auth code for an access token. + 736            // We do this before anything else so that the auth code is invalidated as soon as the request starts, + 737            // and the resulting access token is revoked if we encounter an error. This ends up providing a simpler + 738            // and more flexible interface for TokenStorage implementors. + 739            try { + 740                // Call the token exchange method, passing in a callback which performs additional validation + 741                // on the auth code before it gets exchanged. + 742                $tokenData = $this->tokenStorage->exchangeAuthCodeForAccessToken($bodyParams['code'], function (array $authCode) use ($request, $bodyParams) { + 743                    // Verify that all required parameters are included. + 744                    $requiredParameters = ($this->requirePkce or !empty($authCode['code_challenge'])) ? ['client_id', 'redirect_uri', 'code_verifier'] : ['client_id', 'redirect_uri']; + 745                    $missingRequiredParameters = array_filter($requiredParameters, function ($p) use ($bodyParams) { + 746                        return !array_key_exists($p, $bodyParams) || empty($bodyParams[$p]); + 747                    }); + 748                    if (!empty($missingRequiredParameters)) { + 749                        $this->logger->warning('The exchange request was missing required parameters. Returning an error response.', ['missing' => $missingRequiredParameters]); + 750                        throw IndieAuthException::create(IndieAuthException::INVALID_REQUEST, $request); + 751                    } + 752 + 753                    // Verify that it was issued for the same client_id and redirect_uri + 754                    if ($authCode['client_id'] !== $bodyParams['client_id'] + 755                        || $authCode['redirect_uri'] !== $bodyParams['redirect_uri']) { + 756                        $this->logger->error("The provided client_id and/or redirect_uri did not match those stored in the token."); + 757                        throw IndieAuthException::create(IndieAuthException::INVALID_GRANT, $request); + 758                    } + 759 + 760                    // If the auth code was requested with no code_challenge, but the exchange request provides a  + 761                    // code_verifier, return an error. + 762                    if (!empty($bodyParams['code_verifier']) && empty($authCode['code_challenge'])) { + 763                        $this->logger->error("A code_verifier was provided when trying to exchange an auth code requested without a code_challenge."); + 764                        throw IndieAuthException::create(IndieAuthException::INVALID_GRANT, $request); + 765                    } + 766 + 767                    if ($this->requirePkce or !empty($authCode['code_challenge'])) { + 768                        // Check that the supplied code_verifier hashes to the stored code_challenge + 769                        // TODO: support method = plain as well as S256. + 770                        if (!hash_equals($authCode['code_challenge'], generatePKCECodeChallenge($bodyParams['code_verifier']))) { + 771                            $this->logger->error("The provided code_verifier did not hash to the stored code_challenge"); + 772                            throw IndieAuthException::create(IndieAuthException::INVALID_GRANT, $request); + 773                        } + 774                    } + 775                     + 776                    // Check that scope is not empty. + 777                    if (empty($authCode['scope'])) { + 778                        $this->logger->error("An exchange request for a token with an empty scope was sent to the token endpoint."); + 779                        throw IndieAuthException::create(IndieAuthException::INVALID_GRANT, $request); + 780                    } + 781                }); + 782            } catch (IndieAuthException $e) { + 783                // If an exception was thrown, return a corresponding error response. + 784                return new Response(400, ['content-type' => 'application/json'], json_encode([ + 785                    'error' => $e->getInfo()['error'], + 786                    'error_description' => $e->getMessage() + 787                ])); + 788            } + 789 + 790            if (is_null($tokenData)) { + 791                $this->logger->error('Attempting to exchange an auth code for a token resulted in null.', $bodyParams); + 792                return new Response(400, ['content-type' => 'application/json'], json_encode([ + 793                    'error' => 'invalid_grant', + 794                    'error_description' => 'The provided credentials were not valid.' + 795                ])); + 796            } + 797 + 798            // TODO: return an error if the token doesn’t contain a me key. + 799 + 800            // If everything checked out, return {"me": "https://example.com"} response + 801            return new Response(200, [ + 802                'content-type' => 'application/json', + 803                'cache-control' => 'no-store', + 804            ], json_encode(array_merge([ + 805                // Ensure that the token_type key is present, if tokenStorage doesn’t include it. + 806                'token_type' => 'Bearer' + 807            ], array_filter($tokenData, function (string $k) { + 808                // We should be able to trust the return data from tokenStorage, but there’s no harm in + 809                // preventing code_challenges from leaking, per OAuth2. + 810                return !in_array($k, ['code_challenge', 'code_challenge_method']); + 811            }, ARRAY_FILTER_USE_KEY)))); + 812        } + 813 + 814        return new Response(400, ['content-type' => 'application/json'], json_encode([ + 815            'error' => 'invalid_request', + 816            'error_description' => 'Request to token endpoint was not a valid code exchange request.' + 817        ])); + 818    } + 819 + 820    /** + 821     * Handle Exception + 822     *  + 823     * Turns an instance of `IndieAuthException` into an appropriate instance of `ResponseInterface`. + 824     */ + 825    protected function handleException(IndieAuthException $exception): ResponseInterface { + 826        $exceptionData = $exception->getInfo(); + 827 + 828        if ($exceptionData['statusCode'] == 302) { + 829            // This exception is handled by redirecting to the redirect_uri with error parameters. + 830            $redirectQueryParams = [ + 831                'error' => $exceptionData['error'] ?? 'invalid_request', + 832                'error_description' => (string) $exception + 833            ]; + 834 + 835            // If the state parameter was valid, include it in the error redirect. + 836            if ($exception->getCode() !== IndieAuthException::INVALID_STATE) { + 837                $redirectQueryParams['state'] = $exception->getRequest()->getQueryParams()['state']; + 838            } + 839 + 840            return new Response($exceptionData['statusCode'], [ + 841                'Location' => appendQueryParams((string) $exception->getRequest()->getQueryParams()['redirect_uri'], $redirectQueryParams) + 842            ]); + 843        } else { + 844            // This exception should be shown to the user. + 845            return new Response($exception->getStatusCode(), ['content-type' => 'text/html'], renderTemplate($this->exceptionTemplatePath, [ + 846                'request' => $exception->getRequest(), + 847                'exception' => $exception + 848            ])); + 849        } + 850    } + 851} @@ -1070,7 +1071,7 @@

Legend

ExecutedNot ExecutedDead Code

- Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Fri Jun 18 14:09:49 UTC 2021. + Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Fri Jun 18 14:43:22 UTC 2021.

diff --git a/docs/coverage/Storage/FilesystemJsonStorage.php.html b/docs/coverage/Storage/FilesystemJsonStorage.php.html index eedc1e0..e00ae36 100644 --- a/docs/coverage/Storage/FilesystemJsonStorage.php.html +++ b/docs/coverage/Storage/FilesystemJsonStorage.php.html @@ -52,23 +52,23 @@
0.00%
0 / 1
+
+
+ 46.15% covered (danger) +
+
+ +
46.15%
+
6 / 13
+ CRAP
-
- 53.85% covered (warning) +
+ 84.87% covered (warning)
-
53.85%
-
7 / 13
- CRAP -
-
- 92.31% covered (success) -
-
- -
92.31%
-
96 / 104
+
84.87%
+
101 / 119
@@ -81,23 +81,23 @@
0.00%
0 / 1
+
+
+ 46.15% covered (danger) +
+
+ +
46.15%
+
6 / 13
+ 53.32
-
- 53.85% covered (warning) +
+ 84.87% covered (warning)
-
53.85%
-
7 / 13
- 46.96 -
-
- 92.31% covered (success) -
-
- -
92.31%
-
96 / 104
+
84.87%
+
101 / 119
@@ -152,19 +152,19 @@
0.00%
0 / 1
- 3.03 + 3.02
-
- 85.71% covered (warning) +
+ 87.50% covered (warning)
-
85.71%
-
6 / 7
+
87.50%
+
7 / 8
-  
exchangeAuthCodeForAccessToken +  exchangeAuthCodeForAccessToken
0.00% covered (danger) @@ -173,40 +173,40 @@
0.00%
0 / 1
- 11.04 -
-
- 92.86% covered (success) + 12.44 +
+
+ 77.14% covered (warning)
-
92.86%
-
26 / 28
+
77.14%
+
27 / 35
-  getAccessToken -
-
- 100.00% covered (success) +  getAccessToken +
+
+ 0.00% covered (danger)
-
100.00%
-
1 / 1
- 5 -
-
- 100.00% covered (success) +
0.00%
+
0 / 1
+ 6.20 +
+
+ 63.64% covered (warning)
-
100.00%
-
5 / 5
+
63.64%
+
7 / 11
-  revokeAccessToken +  revokeAccessToken
100.00% covered (success) @@ -223,11 +223,11 @@
100.00%
-
1 / 1
+
2 / 2
-  deleteExpiredTokens +  deleteExpiredTokens
100.00% covered (success) @@ -248,7 +248,7 @@ -  get +  get
0.00% covered (danger) @@ -269,7 +269,7 @@ -  put +  put
100.00% covered (success) @@ -290,7 +290,7 @@ -  delete +  delete
0.00% covered (danger) @@ -311,7 +311,7 @@ -  getPath +  getPath
100.00% covered (success) @@ -332,7 +332,7 @@ -  withLock +  withLock
0.00% covered (danger) @@ -353,7 +353,7 @@ -  hash +  hash
100.00% covered (success) @@ -447,212 +447,236 @@ 66    // TokenStorageInterface Methods. 67 68    public function createAuthCode(array $data): ?string { - 69        $authCode = generateRandomString(self::TOKEN_LENGTH); - 70        $accessToken = $this->hash($authCode); - 71 - 72        if (!array_key_exists('valid_until', $data)) { - 73            $data['valid_until'] = time() + $this->authCodeTtl; - 74        } - 75         - 76        if (!$this->put($accessToken, $data)) { - 77            return null; + 69        $this->logger->info("Creating authorization code.", $data); + 70        $authCode = generateRandomString(self::TOKEN_LENGTH); + 71        $accessToken = $this->hash($authCode); + 72 + 73        // TODO: valid_until should be expire_after(? — look up), and should not be set here, + 74        // as it applies only to access tokens, not auth codes! Auth code TTL should always be  + 75        // the default. + 76        if (!array_key_exists('valid_until', $data)) { + 77            $data['valid_until'] = time() + $this->authCodeTtl; 78        } - 79        return $authCode; - 80    } - 81 - 82    public function exchangeAuthCodeForAccessToken(string $code, callable $validateAuthCode): ?array { - 83        // Hash the auth code to get the theoretical matching access token filename. - 84        $accessToken = $this->hash($code); + 79         + 80        if (!$this->put($accessToken, $data)) { + 81            return null; + 82        } + 83        return $authCode; + 84    } 85 - 86        // Prevent the token file from being read, modified or deleted while we’re working with it. - 87        // r+ to allow reading and writing, but to make sure we don’t create the file if it doesn’t  - 88        // already exist. - 89        return $this->withLock($this->getPath($accessToken), 'r+', function ($fp) use ($accessToken, $validateAuthCode) { - 90            // Read the file contents. - 91            $fileContents = ''; - 92            while ($d = fread($fp, 1024)) { $fileContents .= $d; } - 93 - 94            $data = json_decode($fileContents, true); - 95             - 96            if (!is_array($data)) { return null; } + 86    public function exchangeAuthCodeForAccessToken(string $code, callable $validateAuthCode): ?array { + 87        // Hash the auth code to get the theoretical matching access token filename. + 88        $accessToken = $this->hash($code); + 89 + 90        // Prevent the token file from being read, modified or deleted while we’re working with it. + 91        // r+ to allow reading and writing, but to make sure we don’t create the file if it doesn’t  + 92        // already exist. + 93        return $this->withLock($this->getPath($accessToken), 'r+', function ($fp) use ($accessToken, $validateAuthCode) { + 94            // Read the file contents. + 95            $fileContents = ''; + 96            while ($d = fread($fp, 1024)) { $fileContents .= $d; } 97 - 98            // Make sure the auth code hasn’t already been redeemed. - 99            if ($data['exchanged_at'] ?? false) { return null; } - 100 - 101            // Make sure the auth code isn’t expired. - 102            if (($data['valid_until'] ?? 0) < time()) { return null; } - 103 - 104            // The auth code is valid as far as we know, pass it to the validation callback passed from the - 105            // Server. - 106            try { - 107                $validateAuthCode($data); - 108            } catch (IndieAuthException $e) { - 109                // If there was an issue with the auth code, delete it before bubbling the exception - 110                // up to the Server for handling. We currently have a lock on the file path, so pass - 111                // false to $observeLock to prevent a deadlock. - 112                $this->delete($accessToken, false); - 113                throw $e; - 114            } - 115 - 116            // If the access token is valid, mark it as redeemed and set a new expiry time. - 117            $data['exchanged_at'] = time(); - 118 - 119            if (is_int($data['_access_token_ttl'] ?? null)) { - 120                // This access token has a custom TTL, use that. - 121                $data['valid_until'] = time() + $data['_access_code_ttl']; - 122            } elseif ($this->accessTokenTtl == 0) { - 123                // The token should be valid until explicitly revoked. - 124                $data['valid_until'] = null; - 125            } else { - 126                // Use the default TTL. - 127                $data['valid_until'] = time() + $this->accessTokenTtl; + 98            $data = json_decode($fileContents, true); + 99             + 100            if (!is_array($data)) {  + 101                $this->logger->error('Authoriazation Code data could not be parsed as a JSON object.'); + 102                return null;  + 103            } + 104 + 105            // Make sure the auth code hasn’t already been redeemed. + 106            if ($data['exchanged_at'] ?? false) { + 107                $this->logger->error("This authorization code has already been exchanged."); + 108                return null; + 109            } + 110 + 111            // Make sure the auth code isn’t expired. + 112            if (($data['valid_until'] ?? 0) < time()) { + 113                $this->logger->error("This authorization code has expired."); + 114                return null; + 115            } + 116 + 117            // The auth code is valid as far as we know, pass it to the validation callback passed from the + 118            // Server. + 119            try { + 120                $validateAuthCode($data); + 121            } catch (IndieAuthException $e) { + 122                // If there was an issue with the auth code, delete it before bubbling the exception + 123                // up to the Server for handling. We currently have a lock on the file path, so pass + 124                // false to $observeLock to prevent a deadlock. + 125                $this->logger->info("Deleting authorization code, as it failed the Server-level validation."); + 126                $this->delete($accessToken, false); + 127                throw $e; 128            } 129 - 130            // Write the new file contents, truncating afterwards in case the new data is shorter than the old data. - 131            $jsonData = json_encode($data); - 132            if (rewind($fp) === false) { return null; } - 133            if (fwrite($fp, $jsonData) === false) { return null; } - 134            if (ftruncate($fp, strlen($jsonData)) === false) { return null; } - 135 - 136            // Return the OAuth2-compatible access token data to the Server for passing onto - 137            // the client app. Passed via array_filter to remove the scope key if scope is null. - 138            return array_filter([ - 139                'access_token' => $accessToken, - 140                'scope' => $data['scope'] ?? null, - 141                'me' => $data['me'], - 142                'profile' => $data['profile'] ?? null - 143            ]); - 144        }); - 145    } - 146 - 147    public function getAccessToken(string $token): ?array { - 148        $data = $this->get($token); + 130            // If the access token is valid, mark it as redeemed and set a new expiry time. + 131            $data['exchanged_at'] = time(); + 132 + 133            if (is_int($data['_access_token_ttl'] ?? null)) { + 134                // This access token has a custom TTL, use that. + 135                $data['valid_until'] = time() + $data['_access_code_ttl']; + 136            } elseif ($this->accessTokenTtl == 0) { + 137                // The token should be valid until explicitly revoked. + 138                $data['valid_until'] = null; + 139            } else { + 140                // Use the default TTL. + 141                $data['valid_until'] = time() + $this->accessTokenTtl; + 142            } + 143 + 144            // Write the new file contents, truncating afterwards in case the new data is shorter than the old data. + 145            $jsonData = json_encode($data); + 146            if (rewind($fp) === false) { return null; } + 147            if (fwrite($fp, $jsonData) === false) { return null; } + 148            if (ftruncate($fp, strlen($jsonData)) === false) { return null; } 149 - 150        if (!is_array($data)) { return null; } - 151 - 152        // Check that this is a redeemed access token. - 153        if (($data['exchanged_at'] ?? false) === false) { return null; } - 154 - 155        // Check that the access token is still valid. valid_until=null means it should live until - 156        // explicitly revoked. - 157        if (is_int($data['valid_until']) && $data['valid_until'] < time()) { return null; } - 158 - 159        // The token is valid! - 160        return $data; - 161    } - 162 - 163    public function revokeAccessToken(string $token): bool { - 164        return $this->delete($token); - 165    } - 166 - 167    // Implementation-Specifc Methods. + 150            // Return the OAuth2-compatible access token data to the Server for passing onto + 151            // the client app. Passed via array_filter to remove the scope key if scope is null. + 152            return array_filter([ + 153                'access_token' => $accessToken, + 154                'scope' => $data['scope'] ?? null, + 155                'me' => $data['me'], + 156                'profile' => $data['profile'] ?? null + 157            ]); + 158        }); + 159    } + 160 + 161    public function getAccessToken(string $token): ?array { + 162        $data = $this->get($token); + 163 + 164        if (!is_array($data)) { + 165            $this->logger->error("The access token could not be parsed as a JSON object."); + 166            return null; + 167        } 168 - 169    public function deleteExpiredTokens(): int { - 170        $deleted = 0; - 171 - 172        foreach (new DirectoryIterator($this->path) as $fileInfo) { - 173            if ($fileInfo->isFile() && $fileInfo->getExtension() == 'json') { - 174                // Only delete files which we can lock. - 175                $successfullyDeleted = $this->withLock($fileInfo->getPathname(), 'r', function ($fp) use ($fileInfo) { - 176                    // Read the file, check expiry date! Only unlink if file is expired. - 177                    $fileContents = ''; - 178                    while ($d = fread($fp, 1024)) { $fileContents .= $d; } - 179 - 180                    $data = json_decode($fileContents, true); + 169        // Check that this is a redeemed access token. + 170        if (($data['exchanged_at'] ?? false) === false) { + 171            $this->logger->error("This authorization code has not yet been exchanged for an access token."); + 172            return null; + 173        } + 174 + 175        // Check that the access token is still valid. valid_until=null means it should live until + 176        // explicitly revoked. + 177        if (is_int($data['valid_until']) && $data['valid_until'] < time()) { + 178            $this->logger->error("This access token has expired."); + 179            return null; + 180        } 181 - 182                    if (!is_array($data)) { return; } - 183                     - 184                    // If valid_until is a valid time, and is in the past, delete the token. - 185                    if (is_int($data['valid_until'] ?? null) && $data['valid_until'] < time()) { - 186                        return unlink($fileInfo->getPathname()); - 187                    } - 188                }); - 189 - 190                if ($successfullyDeleted) { $deleted++; } - 191            } - 192        } - 193 - 194        return $deleted; - 195    } - 196 - 197    public function get(string $key): ?array { - 198        $path = $this->getPath($key); - 199 - 200        if (!file_exists($path)) { - 201            return null; - 202        } + 182        // The token is valid! + 183        return $data; + 184    } + 185 + 186    public function revokeAccessToken(string $token): bool { + 187        $this->logger->info("Deleting access token {$token}"); + 188        return $this->delete($token); + 189    } + 190 + 191    // Implementation-Specifc Methods. + 192 + 193    public function deleteExpiredTokens(): int { + 194        $deleted = 0; + 195 + 196        foreach (new DirectoryIterator($this->path) as $fileInfo) { + 197            if ($fileInfo->isFile() && $fileInfo->getExtension() == 'json') { + 198                // Only delete files which we can lock. + 199                $successfullyDeleted = $this->withLock($fileInfo->getPathname(), 'r', function ($fp) use ($fileInfo) { + 200                    // Read the file, check expiry date! Only unlink if file is expired. + 201                    $fileContents = ''; + 202                    while ($d = fread($fp, 1024)) { $fileContents .= $d; } 203 - 204        return $this->withLock($path, 'r', function ($fp) { - 205            $fileContents = ''; - 206            while ($data = fread($fp, 1024)) { - 207                $fileContents .= $data; - 208            } - 209            $result = json_decode($fileContents, true); - 210 - 211            if (is_array($result)) { - 212                return $result; - 213            } - 214 - 215            return null; - 216        }); - 217    } - 218 - 219    public function put(string $key, array $data): bool { - 220        // Ensure that the containing folder exists. - 221        @mkdir($this->path, 0777, true); - 222         - 223        return $this->withLock($this->getPath($key), 'w', function ($fp) use ($data) { - 224            return fwrite($fp, json_encode($data)) !== false; - 225        }); - 226    } + 204                    $data = json_decode($fileContents, true); + 205 + 206                    if (!is_array($data)) { return; } + 207                     + 208                    // If valid_until is a valid time, and is in the past, delete the token. + 209                    if (is_int($data['valid_until'] ?? null) && $data['valid_until'] < time()) { + 210                        return unlink($fileInfo->getPathname()); + 211                    } + 212                }); + 213 + 214                if ($successfullyDeleted) { $deleted++; } + 215            } + 216        } + 217 + 218        return $deleted; + 219    } + 220 + 221    public function get(string $key): ?array { + 222        $path = $this->getPath($key); + 223 + 224        if (!file_exists($path)) { + 225            return null; + 226        } 227 - 228    public function delete(string $key, $observeLock=true): bool { - 229        $path = $this->getPath($key); - 230        if (file_exists($path)) { - 231            if ($observeLock) { - 232                return $this->withLock($path, 'r', function ($fp) use ($path) { - 233                    return unlink($path); - 234                }); - 235            } else { - 236                return unlink($path); + 228        return $this->withLock($path, 'r', function ($fp) { + 229            $fileContents = ''; + 230            while ($data = fread($fp, 1024)) { + 231                $fileContents .= $data; + 232            } + 233            $result = json_decode($fileContents, true); + 234 + 235            if (is_array($result)) { + 236                return $result; 237            } - 238        } - 239        return false; - 240    } - 241 - 242    public function getPath(string $key): string { - 243        // TODO: ensure that the calculated path is a child of $this->path. - 244        return $this->path . "$key.json"; - 245    } - 246 - 247    protected function withLock(string $path, string $mode, callable $callback) { - 248        $fp = @fopen($path, $mode); - 249 - 250        if ($fp === false) { - 251            return null; - 252        } - 253 - 254        // Wait for a lock. - 255        if (flock($fp, LOCK_EX)) { - 256            $return = null; - 257            try { - 258                // Perform whatever action on the file pointer. - 259                $return = $callback($fp); - 260            } finally { - 261                // Regardless of what happens, release the lock. - 262                flock($fp, LOCK_UN); - 263                fclose($fp); - 264            } - 265            return $return; - 266        } - 267        // It wasn’t possible to get a lock. - 268        return null; + 238 + 239            return null; + 240        }); + 241    } + 242 + 243    public function put(string $key, array $data): bool { + 244        // Ensure that the containing folder exists. + 245        @mkdir($this->path, 0777, true); + 246         + 247        return $this->withLock($this->getPath($key), 'w', function ($fp) use ($data) { + 248            return fwrite($fp, json_encode($data)) !== false; + 249        }); + 250    } + 251 + 252    public function delete(string $key, $observeLock=true): bool { + 253        $path = $this->getPath($key); + 254        if (file_exists($path)) { + 255            if ($observeLock) { + 256                return $this->withLock($path, 'r', function ($fp) use ($path) { + 257                    return unlink($path); + 258                }); + 259            } else { + 260                return unlink($path); + 261            } + 262        } + 263        return false; + 264    } + 265 + 266    public function getPath(string $key): string { + 267        // TODO: ensure that the calculated path is a child of $this->path. + 268        return $this->path . "$key.json"; 269    } 270 - 271    protected function hash(string $token): string { - 272        return hash_hmac('sha256', $token, $this->secret); - 273    } - 274} + 271    protected function withLock(string $path, string $mode, callable $callback) { + 272        $fp = @fopen($path, $mode); + 273 + 274        if ($fp === false) { + 275            return null; + 276        } + 277 + 278        // Wait for a lock. + 279        if (flock($fp, LOCK_EX)) { + 280            $return = null; + 281            try { + 282                // Perform whatever action on the file pointer. + 283                $return = $callback($fp); + 284            } finally { + 285                // Regardless of what happens, release the lock. + 286                flock($fp, LOCK_UN); + 287                fclose($fp); + 288            } + 289            return $return; + 290        } + 291        // It wasn’t possible to get a lock. + 292        return null; + 293    } + 294 + 295    protected function hash(string $token): string { + 296        return hash_hmac('sha256', $token, $this->secret); + 297    } + 298} @@ -663,7 +687,7 @@

Legend

ExecutedNot ExecutedDead Code

- Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Fri Jun 18 14:09:49 UTC 2021. + Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Fri Jun 18 14:43:22 UTC 2021.

diff --git a/docs/coverage/Storage/Sqlite3Storage.php.html b/docs/coverage/Storage/Sqlite3Storage.php.html index f05ea4e..1d41db8 100644 --- a/docs/coverage/Storage/Sqlite3Storage.php.html +++ b/docs/coverage/Storage/Sqlite3Storage.php.html @@ -79,7 +79,7 @@

Legend

ExecutedNot ExecutedDead Code

- Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Fri Jun 18 14:09:49 UTC 2021. + Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Fri Jun 18 14:43:22 UTC 2021.

diff --git a/docs/coverage/Storage/TokenStorageInterface.php.html b/docs/coverage/Storage/TokenStorageInterface.php.html index 3bcf7e3..a523843 100644 --- a/docs/coverage/Storage/TokenStorageInterface.php.html +++ b/docs/coverage/Storage/TokenStorageInterface.php.html @@ -235,7 +235,7 @@

Legend

ExecutedNot ExecutedDead Code

- Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Fri Jun 18 14:09:49 UTC 2021. + Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Fri Jun 18 14:43:22 UTC 2021.

diff --git a/docs/coverage/Storage/dashboard.html b/docs/coverage/Storage/dashboard.html index 24b21b6..5a7bd09 100644 --- a/docs/coverage/Storage/dashboard.html +++ b/docs/coverage/Storage/dashboard.html @@ -58,6 +58,7 @@ + Taproot\IndieAuth\Storage\FilesystemJsonStorage84% @@ -74,6 +75,7 @@ + Taproot\IndieAuth\Storage\FilesystemJsonStorage53 @@ -111,9 +113,11 @@ + getAccessToken63% + exchangeAuthCodeForAccessToken77% __construct81% - createAuthCode85% - delete87% + createAuthCode87% + delete87% @@ -130,9 +134,11 @@ + exchangeAuthCodeForAccessToken12 + getAccessToken6 __construct3 createAuthCode3 - delete3 + delete3 @@ -142,7 +148,7 @@
@@ -161,7 +167,7 @@ $(document).ready(function() { .yAxis.tickFormat(d3.format('d')); d3.select('#classCoverageDistribution svg') - .datum(getCoverageDistributionData([0,0,0,0,0,0,0,0,0,0,1,0], "Class Coverage")) + .datum(getCoverageDistributionData([0,0,0,0,0,0,0,0,0,1,0,0], "Class Coverage")) .transition().duration(500).call(chart); nv.utils.windowResize(chart.update); @@ -179,7 +185,7 @@ $(document).ready(function() { .yAxis.tickFormat(d3.format('d')); d3.select('#methodCoverageDistribution svg') - .datum(getCoverageDistributionData([0,0,0,0,0,0,0,0,0,3,3,7], "Method Coverage")) + .datum(getCoverageDistributionData([0,0,0,0,0,0,0,1,1,3,2,6], "Method Coverage")) .transition().duration(500).call(chart); nv.utils.windowResize(chart.update); @@ -229,7 +235,7 @@ $(document).ready(function() { chart.yAxis.axisLabel('Cyclomatic Complexity'); d3.select('#classComplexity svg') - .datum(getComplexityData([[92.3076923076923,46,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage<\/a>"]], 'Class Complexity')) + .datum(getComplexityData([[84.87394957983193,46,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage<\/a>"]], 'Class Complexity')) .transition() .duration(500) .call(chart); @@ -253,7 +259,7 @@ $(document).ready(function() { chart.yAxis.axisLabel('Method Complexity'); d3.select('#methodComplexity svg') - .datum(getComplexityData([[81.81818181818183,3,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::__construct<\/a>"],[100,1,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::setLogger<\/a>"],[85.71428571428571,3,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::createAuthCode<\/a>"],[92.85714285714286,11,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::exchangeAuthCodeForAccessToken<\/a>"],[100,5,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::getAccessToken<\/a>"],[100,1,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::revokeAccessToken<\/a>"],[100,9,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::deleteExpiredTokens<\/a>"],[91.66666666666666,4,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::get<\/a>"],[100,1,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::put<\/a>"],[87.5,3,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::delete<\/a>"],[100,1,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::getPath<\/a>"],[90.9090909090909,3,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::withLock<\/a>"],[100,1,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::hash<\/a>"]], 'Method Complexity')) + .datum(getComplexityData([[81.81818181818183,3,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::__construct<\/a>"],[100,1,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::setLogger<\/a>"],[87.5,3,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::createAuthCode<\/a>"],[77.14285714285715,11,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::exchangeAuthCodeForAccessToken<\/a>"],[63.63636363636363,5,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::getAccessToken<\/a>"],[100,1,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::revokeAccessToken<\/a>"],[100,9,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::deleteExpiredTokens<\/a>"],[91.66666666666666,4,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::get<\/a>"],[100,1,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::put<\/a>"],[87.5,3,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::delete<\/a>"],[100,1,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::getPath<\/a>"],[90.9090909090909,3,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::withLock<\/a>"],[100,1,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::hash<\/a>"]], 'Method Complexity')) .transition() .duration(500) .call(chart); diff --git a/docs/coverage/Storage/index.html b/docs/coverage/Storage/index.html index d21768a..b93d0a6 100644 --- a/docs/coverage/Storage/index.html +++ b/docs/coverage/Storage/index.html @@ -43,23 +43,23 @@ - Total -
-
- 92.31% covered (success) -
-
- -
92.31%
-
96 / 104
+ Total
-
- 53.85% covered (warning) +
+ 84.87% covered (warning)
-
53.85%
-
7 / 13
+
84.87%
+
101 / 119
+
+
+ 46.15% covered (danger) +
+
+ +
46.15%
+
6 / 13
0.00% covered (danger) @@ -71,23 +71,23 @@ - FilesystemJsonStorage.php -
-
- 92.31% covered (success) -
-
- -
92.31%
-
96 / 104
+ FilesystemJsonStorage.php
-
- 53.85% covered (warning) +
+ 84.87% covered (warning)
-
53.85%
-
7 / 13
+
84.87%
+
101 / 119
+
+
+ 46.15% covered (danger) +
+
+ +
46.15%
+
6 / 13
0.00% covered (danger) @@ -137,7 +137,7 @@ High: 90% to 100%

- Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Fri Jun 18 14:09:49 UTC 2021. + Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Fri Jun 18 14:43:22 UTC 2021.

diff --git a/docs/coverage/dashboard.html b/docs/coverage/dashboard.html index 192b480..433c7c2 100644 --- a/docs/coverage/dashboard.html +++ b/docs/coverage/dashboard.html @@ -59,6 +59,7 @@ Taproot\IndieAuth\Middleware\NoOpMiddleware0% Taproot\IndieAuth\IndieAuthException66% + Taproot\IndieAuth\Storage\FilesystemJsonStorage84% @@ -75,6 +76,7 @@ + Taproot\IndieAuth\Storage\FilesystemJsonStorage53 Taproot\IndieAuth\IndieAuthException10 @@ -116,10 +118,12 @@ getExplanation0% trustQueryParams0% process0% + getAccessToken63% + exchangeAuthCodeForAccessToken77% __construct81% create83% - createAuthCode85% - delete87% + createAuthCode87% + delete87% @@ -136,10 +140,12 @@ + exchangeAuthCodeForAccessToken12 + getAccessToken6 trustQueryParams6 __construct3 createAuthCode3 - delete3 + delete3 create2 @@ -150,7 +156,7 @@
@@ -169,7 +175,7 @@ $(document).ready(function() { .yAxis.tickFormat(d3.format('d')); d3.select('#classCoverageDistribution svg') - .datum(getCoverageDistributionData([1,0,0,0,0,0,0,1,0,0,3,4], "Class Coverage")) + .datum(getCoverageDistributionData([1,0,0,0,0,0,0,1,0,1,2,4], "Class Coverage")) .transition().duration(500).call(chart); nv.utils.windowResize(chart.update); @@ -187,7 +193,7 @@ $(document).ready(function() { .yAxis.tickFormat(d3.format('d')); d3.select('#methodCoverageDistribution svg') - .datum(getCoverageDistributionData([3,0,0,0,0,0,0,0,0,4,5,27], "Method Coverage")) + .datum(getCoverageDistributionData([3,0,0,0,0,0,0,1,1,4,4,26], "Method Coverage")) .transition().duration(500).call(chart); nv.utils.windowResize(chart.update); @@ -237,7 +243,7 @@ $(document).ready(function() { chart.yAxis.axisLabel('Cyclomatic Complexity'); d3.select('#classComplexity svg') - .datum(getComplexityData([[100,6,"Taproot\\IndieAuth\\Callback\\DefaultAuthorizationForm<\/a>"],[100,9,"Taproot\\IndieAuth\\Callback\\SingleUserPasswordAuthenticationCallback<\/a>"],[66.66666666666666,8,"Taproot\\IndieAuth\\IndieAuthException<\/a>"],[100,2,"Taproot\\IndieAuth\\Middleware\\ClosureRequestHandler<\/a>"],[96.875,12,"Taproot\\IndieAuth\\Middleware\\DoubleSubmitCookieCsrfMiddleware<\/a>"],[0,1,"Taproot\\IndieAuth\\Middleware\\NoOpMiddleware<\/a>"],[100,2,"Taproot\\IndieAuth\\Middleware\\ResponseRequestHandler<\/a>"],[96.69117647058823,105,"Taproot\\IndieAuth\\Server<\/a>"],[92.3076923076923,46,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage<\/a>"]], 'Class Complexity')) + .datum(getComplexityData([[100,6,"Taproot\\IndieAuth\\Callback\\DefaultAuthorizationForm<\/a>"],[100,9,"Taproot\\IndieAuth\\Callback\\SingleUserPasswordAuthenticationCallback<\/a>"],[66.66666666666666,8,"Taproot\\IndieAuth\\IndieAuthException<\/a>"],[100,2,"Taproot\\IndieAuth\\Middleware\\ClosureRequestHandler<\/a>"],[96.875,12,"Taproot\\IndieAuth\\Middleware\\DoubleSubmitCookieCsrfMiddleware<\/a>"],[0,1,"Taproot\\IndieAuth\\Middleware\\NoOpMiddleware<\/a>"],[100,2,"Taproot\\IndieAuth\\Middleware\\ResponseRequestHandler<\/a>"],[96.7032967032967,105,"Taproot\\IndieAuth\\Server<\/a>"],[84.87394957983193,46,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage<\/a>"]], 'Class Complexity')) .transition() .duration(500) .call(chart); @@ -261,7 +267,7 @@ $(document).ready(function() { chart.yAxis.axisLabel('Method Complexity'); d3.select('#methodComplexity svg') - .datum(getComplexityData([[100,1,"Taproot\\IndieAuth\\Callback\\DefaultAuthorizationForm::__construct<\/a>"],[100,3,"Taproot\\IndieAuth\\Callback\\DefaultAuthorizationForm::showForm<\/a>"],[100,1,"Taproot\\IndieAuth\\Callback\\DefaultAuthorizationForm::transformAuthorizationCode<\/a>"],[100,1,"Taproot\\IndieAuth\\Callback\\DefaultAuthorizationForm::setLogger<\/a>"],[100,4,"Taproot\\IndieAuth\\Callback\\SingleUserPasswordAuthenticationCallback::__construct<\/a>"],[100,5,"Taproot\\IndieAuth\\Callback\\SingleUserPasswordAuthenticationCallback::__invoke<\/a>"],[83.33333333333334,2,"Taproot\\IndieAuth\\IndieAuthException::create<\/a>"],[100,1,"Taproot\\IndieAuth\\IndieAuthException::getStatusCode<\/a>"],[0,1,"Taproot\\IndieAuth\\IndieAuthException::getExplanation<\/a>"],[100,1,"Taproot\\IndieAuth\\IndieAuthException::getInfo<\/a>"],[0,2,"Taproot\\IndieAuth\\IndieAuthException::trustQueryParams<\/a>"],[100,1,"Taproot\\IndieAuth\\IndieAuthException::getRequest<\/a>"],[100,1,"Taproot\\IndieAuth\\Middleware\\ClosureRequestHandler::__construct<\/a>"],[100,1,"Taproot\\IndieAuth\\Middleware\\ClosureRequestHandler::handle<\/a>"],[92.85714285714286,5,"Taproot\\IndieAuth\\Middleware\\DoubleSubmitCookieCsrfMiddleware::__construct<\/a>"],[100,1,"Taproot\\IndieAuth\\Middleware\\DoubleSubmitCookieCsrfMiddleware::setLogger<\/a>"],[100,3,"Taproot\\IndieAuth\\Middleware\\DoubleSubmitCookieCsrfMiddleware::process<\/a>"],[100,3,"Taproot\\IndieAuth\\Middleware\\DoubleSubmitCookieCsrfMiddleware::isValid<\/a>"],[0,1,"Taproot\\IndieAuth\\Middleware\\NoOpMiddleware::process<\/a>"],[100,1,"Taproot\\IndieAuth\\Middleware\\ResponseRequestHandler::__construct<\/a>"],[100,1,"Taproot\\IndieAuth\\Middleware\\ResponseRequestHandler::handle<\/a>"],[100,17,"Taproot\\IndieAuth\\Server::__construct<\/a>"],[100,1,"Taproot\\IndieAuth\\Server::getTokenStorage<\/a>"],[94.61077844311377,67,"Taproot\\IndieAuth\\Server::handleAuthorizationEndpointRequest<\/a>"],[100,17,"Taproot\\IndieAuth\\Server::handleTokenEndpointRequest<\/a>"],[100,3,"Taproot\\IndieAuth\\Server::handleException<\/a>"],[81.81818181818183,3,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::__construct<\/a>"],[100,1,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::setLogger<\/a>"],[85.71428571428571,3,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::createAuthCode<\/a>"],[92.85714285714286,11,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::exchangeAuthCodeForAccessToken<\/a>"],[100,5,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::getAccessToken<\/a>"],[100,1,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::revokeAccessToken<\/a>"],[100,9,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::deleteExpiredTokens<\/a>"],[91.66666666666666,4,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::get<\/a>"],[100,1,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::put<\/a>"],[87.5,3,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::delete<\/a>"],[100,1,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::getPath<\/a>"],[90.9090909090909,3,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::withLock<\/a>"],[100,1,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::hash<\/a>"]], 'Method Complexity')) + .datum(getComplexityData([[100,1,"Taproot\\IndieAuth\\Callback\\DefaultAuthorizationForm::__construct<\/a>"],[100,3,"Taproot\\IndieAuth\\Callback\\DefaultAuthorizationForm::showForm<\/a>"],[100,1,"Taproot\\IndieAuth\\Callback\\DefaultAuthorizationForm::transformAuthorizationCode<\/a>"],[100,1,"Taproot\\IndieAuth\\Callback\\DefaultAuthorizationForm::setLogger<\/a>"],[100,4,"Taproot\\IndieAuth\\Callback\\SingleUserPasswordAuthenticationCallback::__construct<\/a>"],[100,5,"Taproot\\IndieAuth\\Callback\\SingleUserPasswordAuthenticationCallback::__invoke<\/a>"],[83.33333333333334,2,"Taproot\\IndieAuth\\IndieAuthException::create<\/a>"],[100,1,"Taproot\\IndieAuth\\IndieAuthException::getStatusCode<\/a>"],[0,1,"Taproot\\IndieAuth\\IndieAuthException::getExplanation<\/a>"],[100,1,"Taproot\\IndieAuth\\IndieAuthException::getInfo<\/a>"],[0,2,"Taproot\\IndieAuth\\IndieAuthException::trustQueryParams<\/a>"],[100,1,"Taproot\\IndieAuth\\IndieAuthException::getRequest<\/a>"],[100,1,"Taproot\\IndieAuth\\Middleware\\ClosureRequestHandler::__construct<\/a>"],[100,1,"Taproot\\IndieAuth\\Middleware\\ClosureRequestHandler::handle<\/a>"],[92.85714285714286,5,"Taproot\\IndieAuth\\Middleware\\DoubleSubmitCookieCsrfMiddleware::__construct<\/a>"],[100,1,"Taproot\\IndieAuth\\Middleware\\DoubleSubmitCookieCsrfMiddleware::setLogger<\/a>"],[100,3,"Taproot\\IndieAuth\\Middleware\\DoubleSubmitCookieCsrfMiddleware::process<\/a>"],[100,3,"Taproot\\IndieAuth\\Middleware\\DoubleSubmitCookieCsrfMiddleware::isValid<\/a>"],[0,1,"Taproot\\IndieAuth\\Middleware\\NoOpMiddleware::process<\/a>"],[100,1,"Taproot\\IndieAuth\\Middleware\\ResponseRequestHandler::__construct<\/a>"],[100,1,"Taproot\\IndieAuth\\Middleware\\ResponseRequestHandler::handle<\/a>"],[100,17,"Taproot\\IndieAuth\\Server::__construct<\/a>"],[100,1,"Taproot\\IndieAuth\\Server::getTokenStorage<\/a>"],[94.64285714285714,67,"Taproot\\IndieAuth\\Server::handleAuthorizationEndpointRequest<\/a>"],[100,17,"Taproot\\IndieAuth\\Server::handleTokenEndpointRequest<\/a>"],[100,3,"Taproot\\IndieAuth\\Server::handleException<\/a>"],[81.81818181818183,3,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::__construct<\/a>"],[100,1,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::setLogger<\/a>"],[87.5,3,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::createAuthCode<\/a>"],[77.14285714285715,11,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::exchangeAuthCodeForAccessToken<\/a>"],[63.63636363636363,5,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::getAccessToken<\/a>"],[100,1,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::revokeAccessToken<\/a>"],[100,9,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::deleteExpiredTokens<\/a>"],[91.66666666666666,4,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::get<\/a>"],[100,1,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::put<\/a>"],[87.5,3,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::delete<\/a>"],[100,1,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::getPath<\/a>"],[90.9090909090909,3,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::withLock<\/a>"],[100,1,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage::hash<\/a>"]], 'Method Complexity')) .transition() .duration(500) .call(chart); diff --git a/docs/coverage/functions.php.html b/docs/coverage/functions.php.html index a944d14..7a1c0ae 100644 --- a/docs/coverage/functions.php.html +++ b/docs/coverage/functions.php.html @@ -687,7 +687,7 @@

Legend

ExecutedNot ExecutedDead Code

- Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Fri Jun 18 14:09:49 UTC 2021. + Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Fri Jun 18 14:43:22 UTC 2021.

diff --git a/docs/coverage/index.html b/docs/coverage/index.html index 3d45d1f..a066a87 100644 --- a/docs/coverage/index.html +++ b/docs/coverage/index.html @@ -44,21 +44,21 @@ Total
-
- 95.59% covered (success) +
+ 94.00% covered (success)
-
95.59%
-
542 / 567
+
94.00%
+
548 / 583
-
- 75.44% covered (warning) +
+ 73.68% covered (warning)
-
75.44%
-
43 / 57
+
73.68%
+
42 / 57
44.44% covered (danger) @@ -126,23 +126,23 @@ - Storage -
-
- 92.31% covered (success) -
-
- -
92.31%
-
96 / 104
+ Storage
-
- 53.85% covered (warning) +
+ 84.87% covered (warning)
-
53.85%
-
7 / 13
+
84.87%
+
101 / 119
+
+
+ 46.15% covered (danger) +
+
+ +
46.15%
+
6 / 13
0.00% covered (danger) @@ -184,13 +184,13 @@ Server.php
-
- 96.69% covered (success) +
+ 96.70% covered (success)
-
96.69%
-
263 / 272
+
96.70%
+
264 / 273
80.00% covered (warning) @@ -245,7 +245,7 @@ High: 90% to 100%

- Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Fri Jun 18 14:09:49 UTC 2021. + Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Fri Jun 18 14:43:22 UTC 2021.

diff --git a/src/Callback/DefaultAuthorizationForm.php b/src/Callback/DefaultAuthorizationForm.php index 69b7786..8fe5126 100644 --- a/src/Callback/DefaultAuthorizationForm.php +++ b/src/Callback/DefaultAuthorizationForm.php @@ -87,7 +87,7 @@ class DefaultAuthorizationForm implements AuthorizationFormInterface, LoggerAwar public function transformAuthorizationCode(ServerRequestInterface $request, array $code): array { // Add any granted scopes from the form to the code. - $grantedScopes = $request->getParsedBody()['taproot_indieauth_server_scope[]'] ?? []; + $grantedScopes = $request->getParsedBody()['taproot_indieauth_server_scope'] ?? []; // This default implementation naievely accepts any scopes it receives from the form. // You may wish to perform some sort of validation. diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 7e130a2..9f7e6c7 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -554,7 +554,7 @@ EOT 'scope' => 'create', 'redirect_uri' => 'https://app.example.com/indieauth?client_redirect_query_string_param=value' ], [ - 'taproot_indieauth_server_scope[]' => $grantedScopes + 'taproot_indieauth_server_scope' => $grantedScopes ]); $res = $s->handleAuthorizationEndpointRequest($req); @@ -924,7 +924,7 @@ EOT // Build a valid, hashed request without either PKCE parameter. This takes some faffing around // due to the supposedly elegant immutable Request objects. $req = $this->getApprovalRequest(true, true, null, [ - 'taproot_indieauth_server_scope[]' => ['profile'] + 'taproot_indieauth_server_scope' => ['profile'] ]); $params = $req->getQueryParams(); unset($params['code_challenge']);