diff --git a/docs/classes/Taproot-IndieAuth-Server.html b/docs/classes/Taproot-IndieAuth-Server.html index 9b820cf..2bd2638 100644 --- a/docs/classes/Taproot-IndieAuth-Server.html +++ b/docs/classes/Taproot-IndieAuth-Server.html @@ -252,21 +252,21 @@ documentation for both handling methods for further documentation about them.

$handleAuthenticationRequestCallback -  : mixed +  : callable
$handleNonIndieAuthRequest -  : mixed +  : callable
$httpGetWithEffectiveUrl -  : mixed +  : callable
@@ -578,7 +578,7 @@ documentation for both handling methods for further documentation about them.

Server.php : - 117 + 120 @@ -608,17 +608,18 @@ documentation for both handling methods for further documentation about them.

Server.php : - 113 + 115 protected - mixed + callable $handleAuthenticationRequestCallback - +
+ @@ -638,17 +639,18 @@ documentation for both handling methods for further documentation about them.

Server.php : - 115 + 118 protected - mixed + callable $handleNonIndieAuthRequest - +
+ @@ -668,17 +670,18 @@ documentation for both handling methods for further documentation about them.

Server.php : - 111 + 112 protected - mixed + callable $httpGetWithEffectiveUrl - +
+ @@ -728,7 +731,7 @@ documentation for both handling methods for further documentation about them.

Server.php : - 119 + 122 @@ -792,7 +795,7 @@ documentation for both handling methods for further documentation about them.

Server.php : - 188 + 191

Constructor

@@ -909,7 +912,7 @@ as the logger for any objects passed in config which implement Server.php : - 283 + 286 @@ -941,7 +944,7 @@ as the logger for any objects passed in config which implement Server.php : - 322 + 325

Handle Authorization Endpoint Request

@@ -1016,7 +1019,7 @@ error behaviour, one way to do so is to subclass Serve

Handle Token Endpoint Request

@@ -1072,7 +1075,7 @@ error behaviour, one way to do so is to subclass Serve

Handle Exception

diff --git a/docs/coverage/Callback/AuthorizationFormInterface.php.html b/docs/coverage/Callback/AuthorizationFormInterface.php.html index 2d1a27c..b037bf3 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 Mon Jun 14 21:52:39 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 Mon Jun 14 23:14:13 UTC 2021.

diff --git a/docs/coverage/Callback/DefaultAuthorizationForm.php.html b/docs/coverage/Callback/DefaultAuthorizationForm.php.html index d95d247..bbce7af 100644 --- a/docs/coverage/Callback/DefaultAuthorizationForm.php.html +++ b/docs/coverage/Callback/DefaultAuthorizationForm.php.html @@ -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 Mon Jun 14 21:52:39 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 Mon Jun 14 23:14:13 UTC 2021.

diff --git a/docs/coverage/Callback/SingleUserPasswordAuthenticationCallback.php.html b/docs/coverage/Callback/SingleUserPasswordAuthenticationCallback.php.html index 087304e..79a8387 100644 --- a/docs/coverage/Callback/SingleUserPasswordAuthenticationCallback.php.html +++ b/docs/coverage/Callback/SingleUserPasswordAuthenticationCallback.php.html @@ -247,7 +247,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 Mon Jun 14 21:52:39 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 Mon Jun 14 23:14:13 UTC 2021.

diff --git a/docs/coverage/Callback/dashboard.html b/docs/coverage/Callback/dashboard.html index 4a9a97a..02d4b09 100644 --- a/docs/coverage/Callback/dashboard.html +++ b/docs/coverage/Callback/dashboard.html @@ -136,7 +136,7 @@ diff --git a/docs/coverage/Callback/index.html b/docs/coverage/Callback/index.html index ebb3aa5..3914250 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 Mon Jun 14 21:52:39 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 Mon Jun 14 23:14:13 UTC 2021.

diff --git a/docs/coverage/IndieAuthException.php.html b/docs/coverage/IndieAuthException.php.html index c0f4c4b..4e8341f 100644 --- a/docs/coverage/IndieAuthException.php.html +++ b/docs/coverage/IndieAuthException.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 Mon Jun 14 21:52:39 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 Mon Jun 14 23:14:13 UTC 2021.

diff --git a/docs/coverage/Middleware/ClosureRequestHandler.php.html b/docs/coverage/Middleware/ClosureRequestHandler.php.html index 3608171..4b0f4e2 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 Mon Jun 14 21:52:39 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 Mon Jun 14 23:14:13 UTC 2021.

diff --git a/docs/coverage/Middleware/DoubleSubmitCookieCsrfMiddleware.php.html b/docs/coverage/Middleware/DoubleSubmitCookieCsrfMiddleware.php.html index d294d6f..52aba37 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 Mon Jun 14 21:52:39 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 Mon Jun 14 23:14:13 UTC 2021.

diff --git a/docs/coverage/Middleware/NoOpMiddleware.php.html b/docs/coverage/Middleware/NoOpMiddleware.php.html index 38bb38c..8db9a64 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 Mon Jun 14 21:52:39 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 Mon Jun 14 23:14:13 UTC 2021.

diff --git a/docs/coverage/Middleware/ResponseRequestHandler.php.html b/docs/coverage/Middleware/ResponseRequestHandler.php.html index 8e30501..07e7b50 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 Mon Jun 14 21:52:39 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 Mon Jun 14 23:14:13 UTC 2021.

diff --git a/docs/coverage/Middleware/dashboard.html b/docs/coverage/Middleware/dashboard.html index 9f0d0df..0492922 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 ed93ff4..9898ff8 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 Mon Jun 14 21:52:39 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 Mon Jun 14 23:14:13 UTC 2021.

diff --git a/docs/coverage/Server.php.html b/docs/coverage/Server.php.html index de41d91..91ba945 100644 --- a/docs/coverage/Server.php.html +++ b/docs/coverage/Server.php.html @@ -61,13 +61,13 @@
3 / 5
CRAP
-
- 94.90% covered (success) +
+ 94.92% covered (success)
-
94.90%
-
242 / 255
+
94.92%
+
243 / 256
@@ -88,19 +88,19 @@
60.00%
3 / 5
- 83.91 + 86.97
-
- 94.90% covered (success) +
+ 94.92% covered (success)
-
94.90%
-
242 / 255
+
94.92%
+
243 / 256
-  __construct +  __construct
100.00% covered (success) @@ -121,7 +121,7 @@ -  getTokenStorage +  getTokenStorage
100.00% covered (success) @@ -142,7 +142,7 @@ -  handleAuthorizationEndpointRequest +  handleAuthorizationEndpointRequest
0.00% covered (danger) @@ -151,19 +151,19 @@
0.00%
0 / 1
- 51.93 + 55.02
-
- 92.90% covered (success) +
+ 92.95% covered (success)
-
92.90%
-
144 / 155
+
92.95%
+
145 / 156
-  handleTokenEndpointRequest +  handleTokenEndpointRequest
0.00% covered (danger) @@ -184,7 +184,7 @@ -  handleException +  handleException
100.00% covered (success) @@ -320,697 +320,702 @@ 108 109    protected LoggerInterface $logger; 110 - 111    protected $httpGetWithEffectiveUrl; - 112 - 113    protected $handleAuthenticationRequestCallback; - 114 - 115    protected $handleNonIndieAuthRequest; + 111    /** @var callable */ + 112    protected $httpGetWithEffectiveUrl; + 113 + 114    /** @var callable */ + 115    protected $handleAuthenticationRequestCallback; 116 - 117    protected string $exceptionTemplatePath; - 118 - 119    protected string $secret; - 120 - 121    /** - 122     * Constructor - 123     *  - 124     * Server instances are configured by passing a config array to the constructor. - 125     *  - 126     * The following keys are required: - 127     *  - 128     * * `authenticationHandler`: a callable with the signature - 129     *   `function (ServerRequestInterface $request, string $authenticationRedirect, ?string $normalizedMeUrl): array|ResponseInterface`. - 130     *   This function is called on IndieAuth authorization requests, after validating the query parameters. - 131     *    - 132     *   It should check to see if $request is authenticated, then: - 133     *     * If it is authenticated, return an array which MUST have a `me` key, mapping to the  - 134     *       canonical URL of the currently logged-in user. It may additionally have a `profile` key. These - 135     *       keys will be stored in the authorization code and sent to the client, if successful. - 136     *     * If it is not authenticated, either present or redirect to an authentication flow. This flow MUST - 137     *       redirect the logged-in used back to `$authenticationRedirect`. - 138     *    - 139     *   If the request has a valid `me` parameter, the canonicalized version of it is passed as - 140     *   `$normalizedMeUrl`. Otherwise, this parameter is null. This parameter can optionally be used  - 141     *   as a suggestion for which user to log in as in a multi-user authentication flow, but should NOT - 142     *   be considered valid data. - 143     *    - 144     *   If redirecting to an existing authentication flow, this callable can usually be implemented as a - 145     *   closure. The callable may also implement its own authentication logic. For an example, see  - 146     *   `Callback\SingleUserPasswordAuthenticationCallback`. - 147     * * `secret`: A cryptographically random string with a minimum length of 64 characters. Used - 148     *   to hash and subsequently verify request query parameters which get passed around. - 149     * * `tokenStorage`: Either an object implementing `Storage\TokenStorageInterface`, or a string path, - 150     *   which will be passed to `Storage\FilesystemJsonStorage`. This object handles persisting authorization - 151     *   codes and access tokens, as well as implementation-specific parts of the exchange process which are  - 152     *   out of the scope of the Server class (e.g. lifetimes and expiry). Refer to the `Storage\TokenStorageInterface` - 153     *   documentation for more details. - 154     *  - 155     * The following keys may be required depending on which packages you have installed: - 156     *  - 157     * * `httpGetWithEffectiveUrl`: must be a callable with the following signature: - 158     *   `function (string $url): array [ResponseInterface $response, string $effectiveUrl]`, where  - 159     *   `$effectiveUrl` is the final URL after following any redirects (unfortunately, neither the PSR-7 - 160     *   Response nor the PSR-18 Client interfaces offer a standard way of getting this very important - 161     *   data, hence the unusual return signature).  If `guzzlehttp/guzzle` is installed, this parameter - 162     *   will be created automatically. Otherwise, the user must provide their own callable. - 163     *  - 164     * The following keys are optional: - 165     *  - 166     * * `authorizationForm`: an instance of `AuthorizationFormInterface`. Defaults to `DefaultAuthorizationForm`. - 167     *   Refer to that implementation if you wish to replace the consent screen/scope choosing/authorization form. - 168     * * `csrfMiddleware`: an instance of `MiddlewareInterface`, which will be used to CSRF-protect the - 169     *   user-facing authorization flow. By default an instance of `DoubleSubmitCookieCsrfMiddleware`. - 170     *   Refer to that implementation if you want to replace it with your own middleware — you will  - 171     *   likely have to either make sure your middleware sets the same request attribute, or alter your - 172     *   templates accordingly. - 173     * * `exceptionTemplatePath`: string, path to a template which will be used for displaying user-facing - 174     *   errors. Defaults to `../templates/default_exception_response.html.php`, refer to that if you wish - 175     *   to write your own template. - 176     * * `handleNonIndieAuthRequestCallback`: A callback with the following signature: - 177     *   `function (ServerRequestInterface $request): ?ResponseInterface` which will be called if the - 178     *   authorization endpoint gets a request which is not identified as an IndieAuth request or authorization - 179     *   form submission request. You could use this to handle various requests e.g. client-side requests - 180     *   made by your authentication or authorization pages, if it’s not convenient to put them elsewhere. - 181     *   Returning `null` will result in a standard `invalid_request` error being returned. - 182     * * `logger`: An instance of `LoggerInterface`. Will be used for internal logging, and will also be set - 183     *   as the logger for any objects passed in config which implement `LoggerAwareInterface`. - 184     *  - 185     * @param array $config An array of configuration variables - 186     * @return self - 187     */ - 188    public function __construct(array $config) { - 189        $config = array_merge([ - 190            'csrfMiddleware' => new Middleware\DoubleSubmitCookieCsrfMiddleware(self::DEFAULT_CSRF_KEY), - 191            'logger' => null, - 192            self::HANDLE_NON_INDIEAUTH_REQUEST => function (ServerRequestInterface $request) { return null; }, // Default to no-op. - 193            'tokenStorage' => null, - 194            'httpGetWithEffectiveUrl' => null, - 195            'authorizationForm' => new DefaultAuthorizationForm(), - 196            'exceptionTemplatePath' => __DIR__ . '/../templates/default_exception_response.html.php', - 197        ], $config); - 198 - 199        if (!is_string($config['exceptionTemplatePath'])) { - 200            throw new BadMethodCallException("\$config['exceptionTemplatePath'] must be a string (path)."); - 201        } - 202        $this->exceptionTemplatePath = $config['exceptionTemplatePath']; - 203 - 204        $secret = $config['secret'] ?? ''; - 205        if (!is_string($secret) || strlen($secret) < 64) { - 206            throw new BadMethodCallException("\$config['secret'] must be a string with a minimum length of 64 characters."); - 207        } - 208        $this->secret = $secret; - 209 - 210        if (!is_null($config['logger']) && !$config['logger'] instanceof LoggerInterface) { - 211            throw new BadMethodCallException("\$config['logger'] must be an instance of \\Psr\\Log\\LoggerInterface or null."); - 212        } - 213        $this->logger = $config['logger'] ?? new NullLogger(); - 214 - 215        if (!(array_key_exists(self::HANDLE_AUTHENTICATION_REQUEST, $config) and is_callable($config[self::HANDLE_AUTHENTICATION_REQUEST]))) { - 216            throw new BadMethodCallException('$callbacks[\'' . self::HANDLE_AUTHENTICATION_REQUEST .'\'] must be present and callable.'); - 217        } - 218        $this->handleAuthenticationRequestCallback = $config[self::HANDLE_AUTHENTICATION_REQUEST]; - 219         - 220        if (!is_callable($config[self::HANDLE_NON_INDIEAUTH_REQUEST])) { - 221            throw new BadMethodCallException("\$config['" . self::HANDLE_NON_INDIEAUTH_REQUEST . "'] must be callable"); - 222        } - 223        $this->handleNonIndieAuthRequest = $config[self::HANDLE_NON_INDIEAUTH_REQUEST]; - 224 - 225        $tokenStorage = $config['tokenStorage']; - 226        if (!$tokenStorage instanceof Storage\TokenStorageInterface) { - 227            if (is_string($tokenStorage)) { - 228                // Create a default access token storage with a TTL of 7 days. - 229                $tokenStorage = new Storage\FilesystemJsonStorage($tokenStorage, $this->secret); - 230            } else { - 231                throw new BadMethodCallException("\$config['tokenStorage'] parameter must be either a string (path) or an instance of Taproot\IndieAuth\TokenStorageInterface."); - 232            } - 233        } - 234        trySetLogger($tokenStorage, $this->logger); - 235        $this->tokenStorage = $tokenStorage; - 236 - 237        $csrfMiddleware = $config['csrfMiddleware']; - 238        if (!$csrfMiddleware instanceof MiddlewareInterface) { - 239            throw new BadMethodCallException("\$config['csrfMiddleware'] must be null or implement MiddlewareInterface."); - 240        } - 241        trySetLogger($csrfMiddleware, $this->logger); - 242        $this->csrfMiddleware = $csrfMiddleware; - 243 - 244        $httpGetWithEffectiveUrl = $config['httpGetWithEffectiveUrl']; - 245        if (is_null($httpGetWithEffectiveUrl)) { - 246            if (class_exists('\GuzzleHttp\Client')) { - 247                $httpGetWithEffectiveUrl = function (string $uri) { - 248                    // This code can’t be tested, ignore it for coverage purposes. - 249                    // @codeCoverageIgnoreStart - 250                    $resp = (new \GuzzleHttp\Client([ - 251                        \GuzzleHttp\RequestOptions::ALLOW_REDIRECTS => [ - 252                            'max' => 10, - 253                            'strict' => true, - 254                            'referer' => true, - 255                            'track_redirects' => true - 256                        ] - 257                    ]))->get($uri); - 258                     - 259                    $rdh = $resp->getHeader('X-Guzzle-Redirect-History'); - 260                    $effectiveUrl = empty($rdh) ? $uri : array_values($rdh)[count($rdh) - 1]; - 261 - 262                    return [$resp, $effectiveUrl]; - 263                }; - 264            } else { - 265                throw new BadMethodCallException("\$config['httpGetWithEffectiveUrl'] was not provided, and guzzlehttp/guzzle was not installed. Either require guzzlehttp/guzzle, or provide a valid callable."); - 266                // @codeCoverageIgnoreEnd - 267            } - 268        } else { - 269            if (!is_callable($httpGetWithEffectiveUrl)) { - 270                throw new BadMethodCallException("\$config['httpGetWithEffectiveUrl'] must be callable."); - 271            } - 272        } - 273        trySetLogger($httpGetWithEffectiveUrl, $this->logger); - 274        $this->httpGetWithEffectiveUrl = $httpGetWithEffectiveUrl; - 275 - 276        if (!$config['authorizationForm'] instanceof AuthorizationFormInterface) { - 277            throw new BadMethodCallException("When provided, \$config['authorizationForm'] must implement Taproot\IndieAuth\Callback\AuthorizationForm."); - 278        } - 279        $this->authorizationForm = $config['authorizationForm']; - 280        trySetLogger($this->authorizationForm, $this->logger); - 281    } - 282 - 283    public function getTokenStorage(): TokenStorageInterface { - 284        return $this->tokenStorage; - 285    } - 286 - 287    /** - 288     * Handle Authorization Endpoint Request - 289     *  - 290     * This method handles all requests to your authorization endpoint, passing execution off to - 291     * other callbacks when necessary. The logical flow can be summarised as follows: + 117    /** @var callable */ + 118    protected $handleNonIndieAuthRequest; + 119 + 120    protected string $exceptionTemplatePath; + 121 + 122    protected string $secret; + 123 + 124    /** + 125     * Constructor + 126     *  + 127     * Server instances are configured by passing a config array to the constructor. + 128     *  + 129     * The following keys are required: + 130     *  + 131     * * `authenticationHandler`: a callable with the signature + 132     *   `function (ServerRequestInterface $request, string $authenticationRedirect, ?string $normalizedMeUrl): array|ResponseInterface`. + 133     *   This function is called on IndieAuth authorization requests, after validating the query parameters. + 134     *    + 135     *   It should check to see if $request is authenticated, then: + 136     *     * If it is authenticated, return an array which MUST have a `me` key, mapping to the  + 137     *       canonical URL of the currently logged-in user. It may additionally have a `profile` key. These + 138     *       keys will be stored in the authorization code and sent to the client, if successful. + 139     *     * If it is not authenticated, either present or redirect to an authentication flow. This flow MUST + 140     *       redirect the logged-in used back to `$authenticationRedirect`. + 141     *    + 142     *   If the request has a valid `me` parameter, the canonicalized version of it is passed as + 143     *   `$normalizedMeUrl`. Otherwise, this parameter is null. This parameter can optionally be used  + 144     *   as a suggestion for which user to log in as in a multi-user authentication flow, but should NOT + 145     *   be considered valid data. + 146     *    + 147     *   If redirecting to an existing authentication flow, this callable can usually be implemented as a + 148     *   closure. The callable may also implement its own authentication logic. For an example, see  + 149     *   `Callback\SingleUserPasswordAuthenticationCallback`. + 150     * * `secret`: A cryptographically random string with a minimum length of 64 characters. Used + 151     *   to hash and subsequently verify request query parameters which get passed around. + 152     * * `tokenStorage`: Either an object implementing `Storage\TokenStorageInterface`, or a string path, + 153     *   which will be passed to `Storage\FilesystemJsonStorage`. This object handles persisting authorization + 154     *   codes and access tokens, as well as implementation-specific parts of the exchange process which are  + 155     *   out of the scope of the Server class (e.g. lifetimes and expiry). Refer to the `Storage\TokenStorageInterface` + 156     *   documentation for more details. + 157     *  + 158     * The following keys may be required depending on which packages you have installed: + 159     *  + 160     * * `httpGetWithEffectiveUrl`: must be a callable with the following signature: + 161     *   `function (string $url): array [ResponseInterface $response, string $effectiveUrl]`, where  + 162     *   `$effectiveUrl` is the final URL after following any redirects (unfortunately, neither the PSR-7 + 163     *   Response nor the PSR-18 Client interfaces offer a standard way of getting this very important + 164     *   data, hence the unusual return signature).  If `guzzlehttp/guzzle` is installed, this parameter + 165     *   will be created automatically. Otherwise, the user must provide their own callable. + 166     *  + 167     * The following keys are optional: + 168     *  + 169     * * `authorizationForm`: an instance of `AuthorizationFormInterface`. Defaults to `DefaultAuthorizationForm`. + 170     *   Refer to that implementation if you wish to replace the consent screen/scope choosing/authorization form. + 171     * * `csrfMiddleware`: an instance of `MiddlewareInterface`, which will be used to CSRF-protect the + 172     *   user-facing authorization flow. By default an instance of `DoubleSubmitCookieCsrfMiddleware`. + 173     *   Refer to that implementation if you want to replace it with your own middleware — you will  + 174     *   likely have to either make sure your middleware sets the same request attribute, or alter your + 175     *   templates accordingly. + 176     * * `exceptionTemplatePath`: string, path to a template which will be used for displaying user-facing + 177     *   errors. Defaults to `../templates/default_exception_response.html.php`, refer to that if you wish + 178     *   to write your own template. + 179     * * `handleNonIndieAuthRequestCallback`: A callback with the following signature: + 180     *   `function (ServerRequestInterface $request): ?ResponseInterface` which will be called if the + 181     *   authorization endpoint gets a request which is not identified as an IndieAuth request or authorization + 182     *   form submission request. You could use this to handle various requests e.g. client-side requests + 183     *   made by your authentication or authorization pages, if it’s not convenient to put them elsewhere. + 184     *   Returning `null` will result in a standard `invalid_request` error being returned. + 185     * * `logger`: An instance of `LoggerInterface`. Will be used for internal logging, and will also be set + 186     *   as the logger for any objects passed in config which implement `LoggerAwareInterface`. + 187     *  + 188     * @param array $config An array of configuration variables + 189     * @return self + 190     */ + 191    public function __construct(array $config) { + 192        $config = array_merge([ + 193            'csrfMiddleware' => new Middleware\DoubleSubmitCookieCsrfMiddleware(self::DEFAULT_CSRF_KEY), + 194            'logger' => null, + 195            self::HANDLE_NON_INDIEAUTH_REQUEST => function (ServerRequestInterface $request) { return null; }, // Default to no-op. + 196            'tokenStorage' => null, + 197            'httpGetWithEffectiveUrl' => null, + 198            'authorizationForm' => new DefaultAuthorizationForm(), + 199            'exceptionTemplatePath' => __DIR__ . '/../templates/default_exception_response.html.php', + 200        ], $config); + 201 + 202        if (!is_string($config['exceptionTemplatePath'])) { + 203            throw new BadMethodCallException("\$config['exceptionTemplatePath'] must be a string (path)."); + 204        } + 205        $this->exceptionTemplatePath = $config['exceptionTemplatePath']; + 206 + 207        $secret = $config['secret'] ?? ''; + 208        if (!is_string($secret) || strlen($secret) < 64) { + 209            throw new BadMethodCallException("\$config['secret'] must be a string with a minimum length of 64 characters."); + 210        } + 211        $this->secret = $secret; + 212 + 213        if (!is_null($config['logger']) && !$config['logger'] instanceof LoggerInterface) { + 214            throw new BadMethodCallException("\$config['logger'] must be an instance of \\Psr\\Log\\LoggerInterface or null."); + 215        } + 216        $this->logger = $config['logger'] ?? new NullLogger(); + 217 + 218        if (!(array_key_exists(self::HANDLE_AUTHENTICATION_REQUEST, $config) and is_callable($config[self::HANDLE_AUTHENTICATION_REQUEST]))) { + 219            throw new BadMethodCallException('$callbacks[\'' . self::HANDLE_AUTHENTICATION_REQUEST .'\'] must be present and callable.'); + 220        } + 221        $this->handleAuthenticationRequestCallback = $config[self::HANDLE_AUTHENTICATION_REQUEST]; + 222         + 223        if (!is_callable($config[self::HANDLE_NON_INDIEAUTH_REQUEST])) { + 224            throw new BadMethodCallException("\$config['" . self::HANDLE_NON_INDIEAUTH_REQUEST . "'] must be callable"); + 225        } + 226        $this->handleNonIndieAuthRequest = $config[self::HANDLE_NON_INDIEAUTH_REQUEST]; + 227 + 228        $tokenStorage = $config['tokenStorage']; + 229        if (!$tokenStorage instanceof Storage\TokenStorageInterface) { + 230            if (is_string($tokenStorage)) { + 231                // Create a default access token storage with a TTL of 7 days. + 232                $tokenStorage = new Storage\FilesystemJsonStorage($tokenStorage, $this->secret); + 233            } else { + 234                throw new BadMethodCallException("\$config['tokenStorage'] parameter must be either a string (path) or an instance of Taproot\IndieAuth\TokenStorageInterface."); + 235            } + 236        } + 237        trySetLogger($tokenStorage, $this->logger); + 238        $this->tokenStorage = $tokenStorage; + 239 + 240        $csrfMiddleware = $config['csrfMiddleware']; + 241        if (!$csrfMiddleware instanceof MiddlewareInterface) { + 242            throw new BadMethodCallException("\$config['csrfMiddleware'] must be null or implement MiddlewareInterface."); + 243        } + 244        trySetLogger($csrfMiddleware, $this->logger); + 245        $this->csrfMiddleware = $csrfMiddleware; + 246 + 247        $httpGetWithEffectiveUrl = $config['httpGetWithEffectiveUrl']; + 248        if (is_null($httpGetWithEffectiveUrl)) { + 249            if (class_exists('\GuzzleHttp\Client')) { + 250                $httpGetWithEffectiveUrl = function (string $uri) { + 251                    // This code can’t be tested, ignore it for coverage purposes. + 252                    // @codeCoverageIgnoreStart + 253                    $resp = (new \GuzzleHttp\Client([ + 254                        \GuzzleHttp\RequestOptions::ALLOW_REDIRECTS => [ + 255                            'max' => 10, + 256                            'strict' => true, + 257                            'referer' => true, + 258                            'track_redirects' => true + 259                        ] + 260                    ]))->get($uri); + 261                     + 262                    $rdh = $resp->getHeader('X-Guzzle-Redirect-History'); + 263                    $effectiveUrl = empty($rdh) ? $uri : array_values($rdh)[count($rdh) - 1]; + 264 + 265                    return [$resp, $effectiveUrl]; + 266                }; + 267            } else { + 268                throw new BadMethodCallException("\$config['httpGetWithEffectiveUrl'] was not provided, and guzzlehttp/guzzle was not installed. Either require guzzlehttp/guzzle, or provide a valid callable."); + 269                // @codeCoverageIgnoreEnd + 270            } + 271        } else { + 272            if (!is_callable($httpGetWithEffectiveUrl)) { + 273                throw new BadMethodCallException("\$config['httpGetWithEffectiveUrl'] must be callable."); + 274            } + 275        } + 276        trySetLogger($httpGetWithEffectiveUrl, $this->logger); + 277        $this->httpGetWithEffectiveUrl = $httpGetWithEffectiveUrl; + 278 + 279        if (!$config['authorizationForm'] instanceof AuthorizationFormInterface) { + 280            throw new BadMethodCallException("When provided, \$config['authorizationForm'] must implement Taproot\IndieAuth\Callback\AuthorizationForm."); + 281        } + 282        $this->authorizationForm = $config['authorizationForm']; + 283        trySetLogger($this->authorizationForm, $this->logger); + 284    } + 285 + 286    public function getTokenStorage(): TokenStorageInterface { + 287        return $this->tokenStorage; + 288    } + 289 + 290    /** + 291     * Handle Authorization Endpoint Request 292     *  - 293     * * If this request an **auth code exchange for profile information**, validate the request - 294     *   and return a response or error response. - 295     * * Otherwise, proceed, wrapping all execution in CSRF-protection middleware. - 296     * * Validate the request’s indieauth authorization code request parameters, returning an  - 297     *   error response if any are missing or invalid. - 298     * * Call the authentication callback - 299     *     * If the callback returned an instance of ResponseInterface, the user is not currently - 300     *       logged in. Return the Response, which will presumably start an authentication flow. - 301     *     * Otherwise, the callback returned information about the currently logged-in user. Continue. - 302     * * If this request is an authorization form submission, validate the data, store and authorization - 303     *   code and return a redirect response to the client redirect_uri with code data. On an error, return - 304     *   an appropriate error response. - 305     * * Otherwise, fetch the client_id, parse app data if present, validate the `redirect_uri` and present - 306     *   the authorization form/consent screen to the user. - 307     * * If none of the above apply, try calling the non-indieauth request handler. If it returns a Response, - 308     *   return that, otherwise return an error response. - 309     *  - 310     * This route should NOT be wrapped in additional CSRF-protection, due to the need to handle API  - 311     * POST requests from the client. Make sure you call it from a route which is excluded from any - 312     * CSRF-protection you might be using. To customise the CSRF protection used internally, refer to the - 313     * `__construct` config array documentation for the `csrfMiddleware` key. - 314     *  - 315     * Most user-facing errors are thrown as instances of `IndieAuthException`, which are passed off to - 316     * `handleException` to be turned into an instance of `ResponseInterface`. If you want to customise - 317     * error behaviour, one way to do so is to subclass `Server` and override that method. - 318     *  - 319     * @param ServerRequestInterface $request - 320     * @return ResponseInterface - 321     */ - 322    public function handleAuthorizationEndpointRequest(ServerRequestInterface $request): ResponseInterface { - 323        $this->logger->info('Handling an IndieAuth Authorization Endpoint request.'); - 324         - 325        // If it’s a profile information request: - 326        if (isIndieAuthAuthorizationCodeRedeemingRequest($request)) { - 327            $this->logger->info('Handling a request to redeem an authorization code for profile information.'); - 328             - 329            $bodyParams = $request->getParsedBody(); - 330 - 331            if (!isset($bodyParams['code'])) { - 332                $this->logger->warning('The exchange request was missing the code parameter. Returning an error response.'); - 333                return new Response(400, ['content-type' => 'application/json'], json_encode([ - 334                    'error' => 'invalid_request', - 335                    'error_description' => 'The code parameter was missing.' - 336                ])); - 337            } - 338 - 339            // Attempt to internally exchange the provided auth code for an access token. - 340            // We do this before anything else so that the auth code is invalidated as soon as the request starts, - 341            // and the resulting access token is revoked if we encounter an error. This ends up providing a simpler - 342            // and more flexible interface for TokenStorage implementors. - 343            try { - 344                // Call the token exchange method, passing in a callback which performs additional validation - 345                // on the auth code before it gets exchanged. - 346                $tokenData = $this->tokenStorage->exchangeAuthCodeForAccessToken($bodyParams['code'], function (array $authCode) use ($request, $bodyParams) { - 347                    // Verify that all required parameters are included. - 348                    $requiredParameters = ['client_id', 'redirect_uri', 'code_verifier']; - 349                    $missingRequiredParameters = array_filter($requiredParameters, function ($p) use ($bodyParams) { - 350                        return !array_key_exists($p, $bodyParams) || empty($bodyParams[$p]); - 351                    }); - 352                    if (!empty($missingRequiredParameters)) { - 353                        $this->logger->warning('The exchange request was missing required parameters. Returning an error response.', ['missing' => $missingRequiredParameters]); - 354                        throw IndieAuthException::create(IndieAuthException::INVALID_REQUEST, $request); - 355                    } - 356 - 357                    // Verify that it was issued for the same client_id and redirect_uri - 358                    if ($authCode['client_id'] !== $bodyParams['client_id'] - 359                        || $authCode['redirect_uri'] !== $bodyParams['redirect_uri']) { - 360                        $this->logger->error("The provided client_id and/or redirect_uri did not match those stored in the token."); - 361                        throw IndieAuthException::create(IndieAuthException::INVALID_GRANT, $request); - 362                    } - 363 - 364                    // Check that the supplied code_verifier hashes to the stored code_challenge - 365                    // TODO: support method = plain as well as S256. - 366                    if (!hash_equals($authCode['code_challenge'], generatePKCECodeChallenge($bodyParams['code_verifier']))) { - 367                        $this->logger->error("The provided code_verifier did not hash to the stored code_challenge"); - 368                        throw IndieAuthException::create(IndieAuthException::INVALID_GRANT, $request); - 369                    } - 370 - 371                    // Check that this token either grants at most the profile scope. - 372                    $requestedScopes = explode(' ', $authCode['scope'] ?? ''); - 373                    if (!empty($requestedScopes) && $requestedScopes != ['profile']) { - 374                        $this->logger->error("An exchange request for a token granting scopes other than “profile” was sent to the authorization endpoint."); - 375                        throw IndieAuthException::create(IndieAuthException::INVALID_GRANT, $request); - 376                    } - 377                }); - 378            } catch (IndieAuthException $e) { - 379                // If an exception was thrown, return a corresponding error response. - 380                return new Response(400, ['content-type' => 'application/json'], json_encode([ - 381                    'error' => $e->getInfo()['error'], - 382                    'error_description' => $e->getMessage() - 383                ])); - 384            } - 385 - 386            if (is_null($tokenData)) { - 387                $this->logger->error('Attempting to exchange an auth code for a token resulted in null.', $bodyParams); - 388                return new Response(400, ['content-type' => 'application/json'], json_encode([ - 389                    'error' => 'invalid_grant', - 390                    'error_description' => 'The provided credentials were not valid.' - 391                ])); - 392            } - 393 - 394            // TODO: return an error if the token doesn’t contain a me key. - 395 - 396            // If everything checked out, return {"me": "https://example.com"} response - 397            return new Response(200, [ - 398                'content-type' => 'application/json', - 399                'cache-control' => 'no-store', - 400            ], json_encode(array_filter($tokenData, function ($k) { - 401                // Prevent codes exchanged at the authorization endpoint from returning any information other than - 402                // me and profile. - 403                return in_array($k, ['me', 'profile']); - 404            }, ARRAY_FILTER_USE_KEY))); - 405        } - 406 - 407        // Because the special case above isn’t allowed to be CSRF-protected, we have to do some rather silly - 408        // closure gymnastics here to selectively-CSRF-protect requests which do need it. - 409        return $this->csrfMiddleware->process($request, new Middleware\ClosureRequestHandler(function (ServerRequestInterface $request) { - 410            // Wrap the entire user-facing handler in a try/catch block which catches any exception, converts it - 411            // to IndieAuthException if necessary, then passes it to $this->handleException() to be turned into a - 412            // response. - 413            try { - 414                $queryParams = $request->getQueryParams(); - 415 - 416                /** @var ResponseInterface|null $clientIdResponse */ - 417                /** @var string|null $clientIdEffectiveUrl */ - 418                /** @var array|null $clientIdMf2 */ - 419                list($clientIdResponse, $clientIdEffectiveUrl, $clientIdMf2) = [null, null, null]; - 420 - 421                // If this is an authorization or approval request (allowing POST requests as well to accommodate  - 422                // approval requests and custom auth form submission. - 423                if (isIndieAuthAuthorizationRequest($request, ['get', 'post'])) { - 424                    $this->logger->info('Handling an authorization request', ['method' => $request->getMethod()]); - 425 - 426                    // Validate the Client ID. - 427                    if (!isset($queryParams['client_id']) || false === filter_var($queryParams['client_id'], FILTER_VALIDATE_URL) || !isClientIdentifier($queryParams['client_id'])) { - 428                        $this->logger->warning("The client_id provided in an authorization request was not valid.", $queryParams); - 429                        throw IndieAuthException::create(IndieAuthException::INVALID_CLIENT_ID, $request); - 430                    } - 431 - 432                    // Validate the redirect URI. - 433                    if (!isset($queryParams['redirect_uri']) || false === filter_var($queryParams['redirect_uri'], FILTER_VALIDATE_URL)) { - 434                        $this->logger->warning("The client_id provided in an authorization request was not valid.", $queryParams); - 435                        throw IndieAuthException::create(IndieAuthException::INVALID_REDIRECT_URI, $request); - 436                    } - 437 - 438                    // How most errors are handled depends on whether or not the request has a valid redirect_uri. In - 439                    // order to know that, we need to also validate, fetch and parse the client_id. - 440                    // If the request lacks a hash, or if the provided hash was invalid, perform the validation. - 441                    if (!array_key_exists(self::HASH_QUERY_STRING_KEY, $queryParams) || !hash_equals(hashAuthorizationRequestParameters($request, $this->secret), $queryParams[self::HASH_QUERY_STRING_KEY])) { - 442                        // All we need to know at this stage is whether the redirect_uri is valid. If it - 443                        // sufficiently matches the client_id, we don’t (yet) need to fetch the client_id. - 444                        if (!urlComponentsMatch($queryParams['client_id'], $queryParams['redirect_uri'], [PHP_URL_SCHEME, PHP_URL_HOST, PHP_URL_PORT])) { - 445                            // If we do need to fetch the client_id, store the response and effective URL in variables - 446                            // we defined earlier, so they’re available to the approval request code path, which additionally - 447                            // needs to parse client_id for h-app markup. - 448                            try { - 449                                list($clientIdResponse, $clientIdEffectiveUrl) = call_user_func($this->httpGetWithEffectiveUrl, $queryParams['client_id']); - 450                                $clientIdMf2 = Mf2\parse((string) $clientIdResponse->getBody(), $clientIdEffectiveUrl); - 451                            } catch (ClientExceptionInterface | RequestExceptionInterface | NetworkExceptionInterface $e) { - 452                                $this->logger->error("Caught an HTTP exception while trying to fetch the client_id. Returning an error response.", [ - 453                                    'client_id' => $queryParams['client_id'], - 454                                    'exception' => $e->__toString() - 455                                ]); - 456 - 457                                throw IndieAuthException::create(IndieAuthException::HTTP_EXCEPTION_FETCHING_CLIENT_ID, $request, $e); - 458                            } catch (Exception $e) { - 459                                $this->logger->error("Caught an unknown exception while trying to fetch the client_id. Returning an error response.", [ - 460                                    'exception' => $e->__toString() - 461                                ]); - 462 - 463                                throw IndieAuthException::create(IndieAuthException::INTERNAL_EXCEPTION_FETCHING_CLIENT_ID, $request, $e); - 464                            } - 465                             - 466                            // Search for all link@rel=redirect_uri at the client_id. - 467                            $clientIdRedirectUris = []; - 468                            if (array_key_exists('redirect_uri', $clientIdMf2['rels'])) { - 469                                $clientIdRedirectUris = array_merge($clientIdRedirectUris, $clientIdMf2['rels']['redirect_uri']); - 470                            } - 471                             - 472                            foreach (HeaderParser::parse($clientIdResponse->getHeader('Link')) as $link) { - 473                                if (array_key_exists('rel', $link) && mb_strpos(" {$link['rel']} ", " redirect_uri ") !== false) { - 474                                    // Strip off the < > which surround the link URL for some reason. - 475                                    $clientIdRedirectUris[] = substr($link[0], 1, strlen($link[0]) - 2); - 476                                } - 477                            } - 478 - 479                            // If the authority of the redirect_uri does not match the client_id, or exactly match one of their redirect URLs, return an error. - 480                            if (!in_array($queryParams['redirect_uri'], $clientIdRedirectUris)) { - 481                                $this->logger->warning("The provided redirect_uri did not match either the client_id, nor the discovered redirect URIs.", [ - 482                                    'provided_redirect_uri' => $queryParams['redirect_uri'], - 483                                    'provided_client_id' => $queryParams['client_id'], - 484                                    'discovered_redirect_uris' => $clientIdRedirectUris - 485                                ]); - 486 - 487                                throw IndieAuthException::create(IndieAuthException::INVALID_REDIRECT_URI, $request); - 488                            } - 489                        }                         - 490                    } + 293     * This method handles all requests to your authorization endpoint, passing execution off to + 294     * other callbacks when necessary. The logical flow can be summarised as follows: + 295     *  + 296     * * If this request an **auth code exchange for profile information**, validate the request + 297     *   and return a response or error response. + 298     * * Otherwise, proceed, wrapping all execution in CSRF-protection middleware. + 299     * * Validate the request’s indieauth authorization code request parameters, returning an  + 300     *   error response if any are missing or invalid. + 301     * * Call the authentication callback + 302     *     * If the callback returned an instance of ResponseInterface, the user is not currently + 303     *       logged in. Return the Response, which will presumably start an authentication flow. + 304     *     * Otherwise, the callback returned information about the currently logged-in user. Continue. + 305     * * If this request is an authorization form submission, validate the data, store and authorization + 306     *   code and return a redirect response to the client redirect_uri with code data. On an error, return + 307     *   an appropriate error response. + 308     * * Otherwise, fetch the client_id, parse app data if present, validate the `redirect_uri` and present + 309     *   the authorization form/consent screen to the user. + 310     * * If none of the above apply, try calling the non-indieauth request handler. If it returns a Response, + 311     *   return that, otherwise return an error response. + 312     *  + 313     * This route should NOT be wrapped in additional CSRF-protection, due to the need to handle API  + 314     * POST requests from the client. Make sure you call it from a route which is excluded from any + 315     * CSRF-protection you might be using. To customise the CSRF protection used internally, refer to the + 316     * `__construct` config array documentation for the `csrfMiddleware` key. + 317     *  + 318     * Most user-facing errors are thrown as instances of `IndieAuthException`, which are passed off to + 319     * `handleException` to be turned into an instance of `ResponseInterface`. If you want to customise + 320     * error behaviour, one way to do so is to subclass `Server` and override that method. + 321     *  + 322     * @param ServerRequestInterface $request + 323     * @return ResponseInterface + 324     */ + 325    public function handleAuthorizationEndpointRequest(ServerRequestInterface $request): ResponseInterface { + 326        $this->logger->info('Handling an IndieAuth Authorization Endpoint request.'); + 327         + 328        // If it’s a profile information request: + 329        if (isIndieAuthAuthorizationCodeRedeemingRequest($request)) { + 330            $this->logger->info('Handling a request to redeem an authorization code for profile information.'); + 331             + 332            $bodyParams = $request->getParsedBody(); + 333 + 334            if (!isset($bodyParams['code'])) { + 335                $this->logger->warning('The exchange request was missing the code parameter. Returning an error response.'); + 336                return new Response(400, ['content-type' => 'application/json'], json_encode([ + 337                    'error' => 'invalid_request', + 338                    'error_description' => 'The code parameter was missing.' + 339                ])); + 340            } + 341 + 342            // Attempt to internally exchange the provided auth code for an access token. + 343            // We do this before anything else so that the auth code is invalidated as soon as the request starts, + 344            // and the resulting access token is revoked if we encounter an error. This ends up providing a simpler + 345            // and more flexible interface for TokenStorage implementors. + 346            try { + 347                // Call the token exchange method, passing in a callback which performs additional validation + 348                // on the auth code before it gets exchanged. + 349                $tokenData = $this->tokenStorage->exchangeAuthCodeForAccessToken($bodyParams['code'], function (array $authCode) use ($request, $bodyParams) { + 350                    // Verify that all required parameters are included. + 351                    $requiredParameters = ['client_id', 'redirect_uri', 'code_verifier']; + 352                    $missingRequiredParameters = array_filter($requiredParameters, function ($p) use ($bodyParams) { + 353                        return !array_key_exists($p, $bodyParams) || empty($bodyParams[$p]); + 354                    }); + 355                    if (!empty($missingRequiredParameters)) { + 356                        $this->logger->warning('The exchange request was missing required parameters. Returning an error response.', ['missing' => $missingRequiredParameters]); + 357                        throw IndieAuthException::create(IndieAuthException::INVALID_REQUEST, $request); + 358                    } + 359 + 360                    // Verify that it was issued for the same client_id and redirect_uri + 361                    if ($authCode['client_id'] !== $bodyParams['client_id'] + 362                        || $authCode['redirect_uri'] !== $bodyParams['redirect_uri']) { + 363                        $this->logger->error("The provided client_id and/or redirect_uri did not match those stored in the token."); + 364                        throw IndieAuthException::create(IndieAuthException::INVALID_GRANT, $request); + 365                    } + 366 + 367                    // Check that the supplied code_verifier hashes to the stored code_challenge + 368                    // TODO: support method = plain as well as S256. + 369                    if (!hash_equals($authCode['code_challenge'], generatePKCECodeChallenge($bodyParams['code_verifier']))) { + 370                        $this->logger->error("The provided code_verifier did not hash to the stored code_challenge"); + 371                        throw IndieAuthException::create(IndieAuthException::INVALID_GRANT, $request); + 372                    } + 373 + 374                    // Check that this token either grants at most the profile scope. + 375                    $requestedScopes = array_filter(explode(' ', $authCode['scope'] ?? '')); + 376                    if (!empty($requestedScopes) && $requestedScopes != ['profile']) { + 377                        $this->logger->error("An exchange request for a token granting scopes other than “profile” was sent to the authorization endpoint."); + 378                        throw IndieAuthException::create(IndieAuthException::INVALID_GRANT, $request); + 379                    } + 380                }); + 381            } catch (IndieAuthException $e) { + 382                // If an exception was thrown, return a corresponding error response. + 383                return new Response(400, ['content-type' => 'application/json'], json_encode([ + 384                    'error' => $e->getInfo()['error'], + 385                    'error_description' => $e->getMessage() + 386                ])); + 387            } + 388 + 389            if (is_null($tokenData)) { + 390                $this->logger->error('Attempting to exchange an auth code for a token resulted in null.', $bodyParams); + 391                return new Response(400, ['content-type' => 'application/json'], json_encode([ + 392                    'error' => 'invalid_grant', + 393                    'error_description' => 'The provided credentials were not valid.' + 394                ])); + 395            } + 396 + 397            // TODO: return an error if the token doesn’t contain a me key. + 398 + 399            // If everything checked out, return {"me": "https://example.com"} response + 400            return new Response(200, [ + 401                'content-type' => 'application/json', + 402                'cache-control' => 'no-store', + 403            ], json_encode(array_filter($tokenData, function (string $k) { + 404                // Prevent codes exchanged at the authorization endpoint from returning any information other than + 405                // me and profile. + 406                return in_array($k, ['me', 'profile']); + 407            }, ARRAY_FILTER_USE_KEY))); + 408        } + 409 + 410        // Because the special case above isn’t allowed to be CSRF-protected, we have to do some rather silly + 411        // closure gymnastics here to selectively-CSRF-protect requests which do need it. + 412        return $this->csrfMiddleware->process($request, new Middleware\ClosureRequestHandler(function (ServerRequestInterface $request) { + 413            // Wrap the entire user-facing handler in a try/catch block which catches any exception, converts it + 414            // to IndieAuthException if necessary, then passes it to $this->handleException() to be turned into a + 415            // response. + 416            try { + 417                $queryParams = $request->getQueryParams(); + 418 + 419                /** @var ResponseInterface|null $clientIdResponse */ + 420                /** @var string|null $clientIdEffectiveUrl */ + 421                /** @var array|null $clientIdMf2 */ + 422                list($clientIdResponse, $clientIdEffectiveUrl, $clientIdMf2) = [null, null, null]; + 423 + 424                // If this is an authorization or approval request (allowing POST requests as well to accommodate  + 425                // approval requests and custom auth form submission. + 426                if (isIndieAuthAuthorizationRequest($request, ['get', 'post'])) { + 427                    $this->logger->info('Handling an authorization request', ['method' => $request->getMethod()]); + 428 + 429                    // Validate the Client ID. + 430                    if (!isset($queryParams['client_id']) || false === filter_var($queryParams['client_id'], FILTER_VALIDATE_URL) || !isClientIdentifier($queryParams['client_id'])) { + 431                        $this->logger->warning("The client_id provided in an authorization request was not valid.", $queryParams); + 432                        throw IndieAuthException::create(IndieAuthException::INVALID_CLIENT_ID, $request); + 433                    } + 434 + 435                    // Validate the redirect URI. + 436                    if (!isset($queryParams['redirect_uri']) || false === filter_var($queryParams['redirect_uri'], FILTER_VALIDATE_URL)) { + 437                        $this->logger->warning("The client_id provided in an authorization request was not valid.", $queryParams); + 438                        throw IndieAuthException::create(IndieAuthException::INVALID_REDIRECT_URI, $request); + 439                    } + 440 + 441                    // How most errors are handled depends on whether or not the request has a valid redirect_uri. In + 442                    // order to know that, we need to also validate, fetch and parse the client_id. + 443                    // If the request lacks a hash, or if the provided hash was invalid, perform the validation. + 444                    $currentRequestHash = hashAuthorizationRequestParameters($request, $this->secret); + 445                    if (!isset($queryParams[self::HASH_QUERY_STRING_KEY]) or is_null($currentRequestHash) or !hash_equals($currentRequestHash, $queryParams[self::HASH_QUERY_STRING_KEY])) { + 446 + 447                        // All we need to know at this stage is whether the redirect_uri is valid. If it + 448                        // sufficiently matches the client_id, we don’t (yet) need to fetch the client_id. + 449                        if (!urlComponentsMatch($queryParams['client_id'], $queryParams['redirect_uri'], [PHP_URL_SCHEME, PHP_URL_HOST, PHP_URL_PORT])) { + 450                            // If we do need to fetch the client_id, store the response and effective URL in variables + 451                            // we defined earlier, so they’re available to the approval request code path, which additionally + 452                            // needs to parse client_id for h-app markup. + 453                            try { + 454                                list($clientIdResponse, $clientIdEffectiveUrl) = call_user_func($this->httpGetWithEffectiveUrl, $queryParams['client_id']); + 455                                $clientIdMf2 = Mf2\parse((string) $clientIdResponse->getBody(), $clientIdEffectiveUrl); + 456                            } catch (ClientExceptionInterface | RequestExceptionInterface | NetworkExceptionInterface $e) { + 457                                $this->logger->error("Caught an HTTP exception while trying to fetch the client_id. Returning an error response.", [ + 458                                    'client_id' => $queryParams['client_id'], + 459                                    'exception' => $e->__toString() + 460                                ]); + 461 + 462                                throw IndieAuthException::create(IndieAuthException::HTTP_EXCEPTION_FETCHING_CLIENT_ID, $request, $e); + 463                            } catch (Exception $e) { + 464                                $this->logger->error("Caught an unknown exception while trying to fetch the client_id. Returning an error response.", [ + 465                                    'exception' => $e->__toString() + 466                                ]); + 467 + 468                                throw IndieAuthException::create(IndieAuthException::INTERNAL_EXCEPTION_FETCHING_CLIENT_ID, $request, $e); + 469                            } + 470                             + 471                            // Search for all link@rel=redirect_uri at the client_id. + 472                            $clientIdRedirectUris = []; + 473                            if (array_key_exists('redirect_uri', $clientIdMf2['rels'])) { + 474                                $clientIdRedirectUris = array_merge($clientIdRedirectUris, $clientIdMf2['rels']['redirect_uri']); + 475                            } + 476                             + 477                            foreach (HeaderParser::parse($clientIdResponse->getHeader('Link')) as $link) { + 478                                if (array_key_exists('rel', $link) && mb_strpos(" {$link['rel']} ", " redirect_uri ") !== false) { + 479                                    // Strip off the < > which surround the link URL for some reason. + 480                                    $clientIdRedirectUris[] = substr($link[0], 1, strlen($link[0]) - 2); + 481                                } + 482                            } + 483 + 484                            // If the authority of the redirect_uri does not match the client_id, or exactly match one of their redirect URLs, return an error. + 485                            if (!in_array($queryParams['redirect_uri'], $clientIdRedirectUris)) { + 486                                $this->logger->warning("The provided redirect_uri did not match either the client_id, nor the discovered redirect URIs.", [ + 487                                    'provided_redirect_uri' => $queryParams['redirect_uri'], + 488                                    'provided_client_id' => $queryParams['client_id'], + 489                                    'discovered_redirect_uris' => $clientIdRedirectUris + 490                                ]); 491 - 492                    // From now on, we can assume that redirect_uri is valid. Any IndieAuth-related errors should be - 493                    // reported by redirecting to redirect_uri with error parameters. - 494 - 495                    // Validate the state parameter. - 496                    if (!isValidState($queryParams['state'])) { - 497                        $this->logger->warning("The state provided in an authorization request was not valid.", $queryParams); - 498                        throw IndieAuthException::create(IndieAuthException::INVALID_STATE, $request); - 499                    } - 500 - 501                    // Validate code_challenge parameter. - 502                    if (!isValidCodeChallenge($queryParams['code_challenge'])) { - 503                        $this->logger->warning("The code_challenge provided in an authorization request was not valid.", $queryParams); - 504                        throw IndieAuthException::create(IndieAuthException::INVALID_CODE_CHALLENGE, $request); - 505                    } - 506 - 507                    // From now on, any redirect error responses should include the state parameter. - 508                    // This is handled automatically in `handleException()` and is only noted here - 509                    // for reference. - 510 - 511                    // Validate the scope parameter, if provided. - 512                    if (array_key_exists('scope', $queryParams) && !isValidScope($queryParams['scope'])) { - 513                        $this->logger->warning("The scope provided in an authorization request was not valid.", $queryParams); - 514                        throw IndieAuthException::create(IndieAuthException::INVALID_SCOPE, $request); - 515                    } - 516 - 517                    // Normalise the me parameter, if it exists. - 518                    if (array_key_exists('me', $queryParams)) { - 519                        $queryParams['me'] = IndieAuthClient::normalizeMeURL($queryParams['me']); - 520                        // If the me parameter is not a valid profile URL, ignore it. - 521                        if (false === $queryParams['me'] || !isProfileUrl($queryParams['me'])) { - 522                            $queryParams['me'] = null; - 523                        } - 524                    } - 525 - 526                    // Build a URL containing the indieauth authorization request parameters, hashing them - 527                    // to protect them from being changed. - 528                    // Make a hash of the protected indieauth-specific parameters. - 529                    $hash = hashAuthorizationRequestParameters($request, $this->secret); - 530                    // Operate on a copy of $queryParams, otherwise requests will always have a valid hash! - 531                    $redirectQueryParams = $queryParams; - 532                    $redirectQueryParams[self::HASH_QUERY_STRING_KEY] = $hash; - 533                    $authenticationRedirect = $request->getUri()->withQuery(buildQueryString($redirectQueryParams))->__toString(); - 534                     - 535                    // User-facing requests always start by calling the authentication request callback. - 536                    $this->logger->info('Calling handle_authentication_request callback'); - 537                    $authenticationResult = call_user_func($this->handleAuthenticationRequestCallback, $request, $authenticationRedirect, $queryParams['me'] ?? null); - 538                     - 539                    // If the authentication handler returned a Response, return that as-is. - 540                    if ($authenticationResult instanceof ResponseInterface) { - 541                        return $authenticationResult; - 542                    } elseif (is_array($authenticationResult)) { - 543                        // Check the resulting array for errors. - 544                        if (!array_key_exists('me', $authenticationResult)) { - 545                            $this->logger->error('The handle_authentication_request callback returned an array with no me key.', ['array' => $authenticationResult]); - 546                            throw IndieAuthException::create(IndieAuthException::AUTHENTICATION_CALLBACK_MISSING_ME_PARAM, $request); - 547                        } - 548 - 549                        // If this is a POST request sent from the authorization (i.e. scope-choosing) form: - 550                        if (isAuthorizationApprovalRequest($request)) { - 551                            // Authorization approval requests MUST include a hash protecting the sensitive IndieAuth - 552                            // authorization request parameters from being changed, e.g. by a malicious script which - 553                            // found its way onto the authorization form. - 554                            if (!array_key_exists(self::HASH_QUERY_STRING_KEY, $queryParams)) { - 555                                $this->logger->warning("An authorization approval request did not have a " . self::HASH_QUERY_STRING_KEY . " parameter."); - 556                                throw IndieAuthException::create(IndieAuthException::AUTHORIZATION_APPROVAL_REQUEST_MISSING_HASH, $request); - 557                            } - 558 - 559                            $expectedHash = hashAuthorizationRequestParameters($request, $this->secret); - 560                            if (!hash_equals($expectedHash, $queryParams[self::HASH_QUERY_STRING_KEY])) { - 561                                $this->logger->warning("The hash provided in the URL was invalid!", [ - 562                                    'expected' => $expectedHash, - 563                                    'actual' => $queryParams[self::HASH_QUERY_STRING_KEY] - 564                                ]); - 565                                throw IndieAuthException::create(IndieAuthException::AUTHORIZATION_APPROVAL_REQUEST_INVALID_HASH, $request); - 566                            } - 567                             - 568                            // Assemble the data for the authorization code, store it somewhere persistent. - 569                            $code = array_merge($authenticationResult, [ - 570                                'client_id' => $queryParams['client_id'], - 571                                'redirect_uri' => $queryParams['redirect_uri'], - 572                                'state' => $queryParams['state'], - 573                                'code_challenge' => $queryParams['code_challenge'], - 574                                'code_challenge_method' => $queryParams['code_challenge_method'], - 575                                'requested_scope' => $queryParams['scope'] ?? '', - 576                            ]); - 577 - 578                            // Pass it to the auth code customisation callback. - 579                            $code = $this->authorizationForm->transformAuthorizationCode($request, $code); - 580 - 581                            // Store the authorization code. - 582                            $authCode = $this->tokenStorage->createAuthCode($code); - 583                            if (is_null($authCode)) { - 584                                // If saving the authorization code failed silently, there isn’t much we can do about it, - 585                                // but should at least log and return an error. - 586                                $this->logger->error("Saving the authorization code failed and returned false without raising an exception."); - 587                                throw IndieAuthException::create(IndieAuthException::INTERNAL_ERROR_REDIRECT, $request); - 588                            } - 589                             - 590                            // Return a redirect to the client app. - 591                            return new Response(302, [ - 592                                'Location' => appendQueryParams($queryParams['redirect_uri'], [ - 593                                    'code' => $authCode, - 594                                    'state' => $code['state'] - 595                                ]), - 596                                'Cache-control' => 'no-cache' - 597                            ]); - 598                        } - 599 - 600                        // Otherwise, the user is authenticated and needs to authorize the client app + choose scopes. - 601 - 602                        // Fetch the client_id URL to find information about the client to present to the user. - 603                        // TODO: in order to comply with https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1, - 604                        // it may be necessary to do this before returning any other kind of error response, as, per - 605                        // the spec, errors should only be shown to the user if the client_id and redirect_uri parameters - 606                        // are missing or invalid. Otherwise, they should be sent back to the client with an error - 607                        // redirect response. - 608                        if (is_null($clientIdResponse) || is_null($clientIdEffectiveUrl) || is_null($clientIdMf2)) { - 609                            try { - 610                                /** @var ResponseInterface $clientIdResponse */ - 611                                list($clientIdResponse, $clientIdEffectiveUrl) = call_user_func($this->httpGetWithEffectiveUrl, $queryParams['client_id']); - 612                                $clientIdMf2 = Mf2\parse((string) $clientIdResponse->getBody(), $clientIdEffectiveUrl); - 613                            } catch (ClientExceptionInterface | RequestExceptionInterface | NetworkExceptionInterface $e) { - 614                                $this->logger->error("Caught an HTTP exception while trying to fetch the client_id. Returning an error response.", [ - 615                                    'client_id' => $queryParams['client_id'], - 616                                    'exception' => $e->__toString() - 617                                ]); - 618                                 - 619                                // At this point in the flow, we’ve already guaranteed that the redirect_uri is valid, - 620                                // so in theory we should report these errors by redirecting there. - 621                                throw IndieAuthException::create(IndieAuthException::INTERNAL_ERROR_REDIRECT, $request, $e); - 622                            } catch (Exception $e) { - 623                                $this->logger->error("Caught an unknown exception while trying to fetch the client_id. Returning an error response.", [ - 624                                    'exception' => $e->__toString() - 625                                ]); - 626     - 627                                throw IndieAuthException::create(IndieAuthException::INTERNAL_ERROR_REDIRECT, $request, $e); - 628                            } - 629                        } - 630                         - 631                        // Search for an h-app with u-url matching the client_id. - 632                        $clientHApps = M\findMicroformatsByProperty(M\findMicroformatsByType($clientIdMf2, 'h-app'), 'url', $queryParams['client_id']); - 633                        $clientHApp = empty($clientHApps) ? null : $clientHApps[0]; - 634 - 635                        // Present the authorization UI. - 636                        return $this->authorizationForm->showForm($request, $authenticationResult, $authenticationRedirect, $clientHApp) - 637                                ->withAddedHeader('Cache-control', 'no-cache'); - 638                    } - 639                } - 640 - 641                // If the request isn’t an IndieAuth Authorization or Code-redeeming request, it’s either an invalid - 642                // request or something to do with a custom auth handler (e.g. sending a one-time code in an email.) - 643                $nonIndieAuthRequestResult = call_user_func($this->handleNonIndieAuthRequest, $request); - 644                if ($nonIndieAuthRequestResult instanceof ResponseInterface) { - 645                    return $nonIndieAuthRequestResult; - 646                } else { - 647                    // In this code path we have not validated the redirect_uri, so show a regular error page - 648                    // rather than returning a redirect error. - 649                    throw IndieAuthException::create(IndieAuthException::INTERNAL_ERROR, $request); - 650                } - 651            } catch (IndieAuthException $e) { - 652                // All IndieAuthExceptions will already have been logged. - 653                return $this->handleException($e); - 654            } catch (Exception $e) { - 655                // Unknown exceptions will not have been logged; do so now. - 656                $this->logger->error("Caught unknown exception: {$e}"); - 657                return $this->handleException(IndieAuthException::create(0, $request, $e)); - 658            } - 659        }));     - 660    } - 661 - 662    /** - 663     * Handle Token Endpoint Request - 664     *  - 665     * Handles requests to the IndieAuth token endpoint. The logical flow can be summarised as follows: - 666     *  - 667     * * Check that the request is a code redeeming request. Return an error if not. - 668     * * Ensure that all required parameters are present. Return an error if not. - 669     * * Attempt to exchange the `code` parameter for an access token. Return an error if it fails. - 670     * * 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. - 671     * * 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. - 672     * * Make sure the granted scope stored in the auth code is not empty. If it is, revoke the access token and return an error. - 673     * * Otherwise, return a success response containing information about the issued access token. - 674     *  - 675     * This method must NOT be CSRF-protected as it accepts external requests from client apps. - 676     *  - 677     * @param ServerRequestInterface $request - 678     * @return ResponseInterface - 679     */ - 680    public function handleTokenEndpointRequest(ServerRequestInterface $request): ResponseInterface { - 681        if (isIndieAuthAuthorizationCodeRedeemingRequest($request)) { - 682            $this->logger->info('Handling a request to redeem an authorization code for profile information.'); - 683             - 684            $bodyParams = $request->getParsedBody(); - 685 - 686            if (!isset($bodyParams['code'])) { - 687                $this->logger->warning('The exchange request was missing the code parameter. Returning an error response.'); - 688                return new Response(400, ['content-type' => 'application/json'], json_encode([ - 689                    'error' => 'invalid_request', - 690                    'error_description' => 'The code parameter was missing.' - 691                ])); - 692            } - 693 - 694            // Attempt to internally exchange the provided auth code for an access token. - 695            // We do this before anything else so that the auth code is invalidated as soon as the request starts, - 696            // and the resulting access token is revoked if we encounter an error. This ends up providing a simpler - 697            // and more flexible interface for TokenStorage implementors. - 698            try { - 699                // Call the token exchange method, passing in a callback which performs additional validation - 700                // on the auth code before it gets exchanged. - 701                $tokenData = $this->tokenStorage->exchangeAuthCodeForAccessToken($bodyParams['code'], function (array $authCode) use ($request, $bodyParams) { - 702                    // Verify that all required parameters are included. - 703                    $requiredParameters = ['client_id', 'redirect_uri', 'code_verifier']; - 704                    $missingRequiredParameters = array_filter($requiredParameters, function ($p) use ($bodyParams) { - 705                        return !array_key_exists($p, $bodyParams) || empty($bodyParams[$p]); - 706                    }); - 707                    if (!empty($missingRequiredParameters)) { - 708                        $this->logger->warning('The exchange request was missing required parameters. Returning an error response.', ['missing' => $missingRequiredParameters]); - 709                        throw IndieAuthException::create(IndieAuthException::INVALID_REQUEST, $request); - 710                    } - 711 - 712                    // Verify that it was issued for the same client_id and redirect_uri - 713                    if ($authCode['client_id'] !== $bodyParams['client_id'] - 714                        || $authCode['redirect_uri'] !== $bodyParams['redirect_uri']) { - 715                        $this->logger->error("The provided client_id and/or redirect_uri did not match those stored in the token."); - 716                        throw IndieAuthException::create(IndieAuthException::INVALID_GRANT, $request); - 717                    } - 718 - 719                    // Check that the supplied code_verifier hashes to the stored code_challenge - 720                    // TODO: support method = plain as well as S256. - 721                    if (!hash_equals($authCode['code_challenge'], generatePKCECodeChallenge($bodyParams['code_verifier']))) { - 722                        $this->logger->error("The provided code_verifier did not hash to the stored code_challenge"); - 723                        throw IndieAuthException::create(IndieAuthException::INVALID_GRANT, $request); - 724                    } - 725 - 726                    // Check that scope is not empty. - 727                    if (empty($authCode['scope'])) { - 728                        $this->logger->error("An exchange request for a token with an empty scope was sent to the token endpoint."); - 729                        throw IndieAuthException::create(IndieAuthException::INVALID_GRANT, $request); - 730                    } - 731                }); - 732            } catch (IndieAuthException $e) { - 733                // If an exception was thrown, return a corresponding error response. - 734                return new Response(400, ['content-type' => 'application/json'], json_encode([ - 735                    'error' => $e->getInfo()['error'], - 736                    'error_description' => $e->getMessage() - 737                ])); - 738            } - 739 - 740            if (is_null($tokenData)) { - 741                $this->logger->error('Attempting to exchange an auth code for a token resulted in null.', $bodyParams); - 742                return new Response(400, ['content-type' => 'application/json'], json_encode([ - 743                    'error' => 'invalid_grant', - 744                    'error_description' => 'The provided credentials were not valid.' - 745                ])); - 746            } - 747 - 748            // TODO: return an error if the token doesn’t contain a me key. - 749 - 750            // If everything checked out, return {"me": "https://example.com"} response - 751            return new Response(200, [ - 752                'content-type' => 'application/json', - 753                'cache-control' => 'no-store', - 754            ], json_encode(array_merge([ - 755                // Ensure that the token_type key is present, if tokenStorage doesn’t include it. - 756                'token_type' => 'Bearer' - 757            ], array_filter($tokenData, function ($k) { - 758                // We should be able to trust the return data from tokenStorage, but there’s no harm in - 759                // preventing code_challenges from leaking, per OAuth2. - 760                return !in_array($k, ['code_challenge', 'code_challenge_method']); - 761            }, ARRAY_FILTER_USE_KEY)))); - 762        } - 763 - 764        return new Response(400, ['content-type' => 'application/json'], json_encode([ - 765            'error' => 'invalid_request', - 766            'error_description' => 'Request to token endpoint was not a valid code exchange request.' - 767        ])); - 768    } - 769 - 770    /** - 771     * Handle Exception - 772     *  - 773     * Turns an instance of `IndieAuthException` into an appropriate instance of `ResponseInterface`. - 774     */ - 775    protected function handleException(IndieAuthException $exception): ResponseInterface { - 776        $exceptionData = $exception->getInfo(); - 777 - 778        if ($exceptionData['statusCode'] == 302) { - 779            // This exception is handled by redirecting to the redirect_uri with error parameters. - 780            $redirectQueryParams = [ - 781                'error' => $exceptionData['error'] ?? 'invalid_request', - 782                'error_description' => (string) $exception - 783            ]; - 784 - 785            // If the state parameter was valid, include it in the error redirect. - 786            if ($exception->getCode() !== IndieAuthException::INVALID_STATE) { - 787                $redirectQueryParams['state'] = $exception->getRequest()->getQueryParams()['state']; - 788            } + 492                                throw IndieAuthException::create(IndieAuthException::INVALID_REDIRECT_URI, $request); + 493                            } + 494                        }                         + 495                    } + 496 + 497                    // From now on, we can assume that redirect_uri is valid. Any IndieAuth-related errors should be + 498                    // reported by redirecting to redirect_uri with error parameters. + 499 + 500                    // Validate the state parameter. + 501                    if (!isValidState($queryParams['state'])) { + 502                        $this->logger->warning("The state provided in an authorization request was not valid.", $queryParams); + 503                        throw IndieAuthException::create(IndieAuthException::INVALID_STATE, $request); + 504                    } + 505 + 506                    // Validate code_challenge parameter. + 507                    if (!isValidCodeChallenge($queryParams['code_challenge'])) { + 508                        $this->logger->warning("The code_challenge provided in an authorization request was not valid.", $queryParams); + 509                        throw IndieAuthException::create(IndieAuthException::INVALID_CODE_CHALLENGE, $request); + 510                    } + 511 + 512                    // From now on, any redirect error responses should include the state parameter. + 513                    // This is handled automatically in `handleException()` and is only noted here + 514                    // for reference. + 515 + 516                    // Validate the scope parameter, if provided. + 517                    if (array_key_exists('scope', $queryParams) && !isValidScope($queryParams['scope'])) { + 518                        $this->logger->warning("The scope provided in an authorization request was not valid.", $queryParams); + 519                        throw IndieAuthException::create(IndieAuthException::INVALID_SCOPE, $request); + 520                    } + 521 + 522                    // Normalise the me parameter, if it exists. + 523                    if (array_key_exists('me', $queryParams)) { + 524                        $queryParams['me'] = IndieAuthClient::normalizeMeURL($queryParams['me']); + 525                        // If the me parameter is not a valid profile URL, ignore it. + 526                        if (false === $queryParams['me'] || !isProfileUrl($queryParams['me'])) { + 527                            $queryParams['me'] = null; + 528                        } + 529                    } + 530 + 531                    // Build a URL containing the indieauth authorization request parameters, hashing them + 532                    // to protect them from being changed. + 533                    // Make a hash of the protected indieauth-specific parameters. + 534                    $hash = hashAuthorizationRequestParameters($request, $this->secret); + 535                    // Operate on a copy of $queryParams, otherwise requests will always have a valid hash! + 536                    $redirectQueryParams = $queryParams; + 537                    $redirectQueryParams[self::HASH_QUERY_STRING_KEY] = $hash; + 538                    $authenticationRedirect = $request->getUri()->withQuery(buildQueryString($redirectQueryParams))->__toString(); + 539                     + 540                    // User-facing requests always start by calling the authentication request callback. + 541                    $this->logger->info('Calling handle_authentication_request callback'); + 542                    $authenticationResult = call_user_func($this->handleAuthenticationRequestCallback, $request, $authenticationRedirect, $queryParams['me'] ?? null); + 543                     + 544                    // If the authentication handler returned a Response, return that as-is. + 545                    if ($authenticationResult instanceof ResponseInterface) { + 546                        return $authenticationResult; + 547                    } elseif (is_array($authenticationResult)) { + 548                        // Check the resulting array for errors. + 549                        if (!array_key_exists('me', $authenticationResult)) { + 550                            $this->logger->error('The handle_authentication_request callback returned an array with no me key.', ['array' => $authenticationResult]); + 551                            throw IndieAuthException::create(IndieAuthException::AUTHENTICATION_CALLBACK_MISSING_ME_PARAM, $request); + 552                        } + 553 + 554                        // If this is a POST request sent from the authorization (i.e. scope-choosing) form: + 555                        if (isAuthorizationApprovalRequest($request)) { + 556                            // Authorization approval requests MUST include a hash protecting the sensitive IndieAuth + 557                            // authorization request parameters from being changed, e.g. by a malicious script which + 558                            // found its way onto the authorization form. + 559                            if (!array_key_exists(self::HASH_QUERY_STRING_KEY, $queryParams)) { + 560                                $this->logger->warning("An authorization approval request did not have a " . self::HASH_QUERY_STRING_KEY . " parameter."); + 561                                throw IndieAuthException::create(IndieAuthException::AUTHORIZATION_APPROVAL_REQUEST_MISSING_HASH, $request); + 562                            } + 563 + 564                            $expectedHash = hashAuthorizationRequestParameters($request, $this->secret); + 565                            if (!isset($queryParams[self::HASH_QUERY_STRING_KEY]) or is_null($expectedHash) or !hash_equals($expectedHash, $queryParams[self::HASH_QUERY_STRING_KEY])) { + 566                                $this->logger->warning("The hash provided in the URL was invalid!", [ + 567                                    'expected' => $expectedHash, + 568                                    'actual' => $queryParams[self::HASH_QUERY_STRING_KEY] + 569                                ]); + 570                                throw IndieAuthException::create(IndieAuthException::AUTHORIZATION_APPROVAL_REQUEST_INVALID_HASH, $request); + 571                            } + 572                             + 573                            // Assemble the data for the authorization code, store it somewhere persistent. + 574                            $code = array_merge($authenticationResult, [ + 575                                'client_id' => $queryParams['client_id'], + 576                                'redirect_uri' => $queryParams['redirect_uri'], + 577                                'state' => $queryParams['state'], + 578                                'code_challenge' => $queryParams['code_challenge'], + 579                                'code_challenge_method' => $queryParams['code_challenge_method'], + 580                                'requested_scope' => $queryParams['scope'] ?? '', + 581                            ]); + 582 + 583                            // Pass it to the auth code customisation callback. + 584                            $code = $this->authorizationForm->transformAuthorizationCode($request, $code); + 585 + 586                            // Store the authorization code. + 587                            $authCode = $this->tokenStorage->createAuthCode($code); + 588                            if (is_null($authCode)) { + 589                                // If saving the authorization code failed silently, there isn’t much we can do about it, + 590                                // but should at least log and return an error. + 591                                $this->logger->error("Saving the authorization code failed and returned false without raising an exception."); + 592                                throw IndieAuthException::create(IndieAuthException::INTERNAL_ERROR_REDIRECT, $request); + 593                            } + 594                             + 595                            // Return a redirect to the client app. + 596                            return new Response(302, [ + 597                                'Location' => appendQueryParams($queryParams['redirect_uri'], [ + 598                                    'code' => $authCode, + 599                                    'state' => $code['state'] + 600                                ]), + 601                                'Cache-control' => 'no-cache' + 602                            ]); + 603                        } + 604 + 605                        // Otherwise, the user is authenticated and needs to authorize the client app + choose scopes. + 606 + 607                        // Fetch the client_id URL to find information about the client to present to the user. + 608                        // TODO: in order to comply with https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1, + 609                        // it may be necessary to do this before returning any other kind of error response, as, per + 610                        // the spec, errors should only be shown to the user if the client_id and redirect_uri parameters + 611                        // are missing or invalid. Otherwise, they should be sent back to the client with an error + 612                        // redirect response. + 613                        if (is_null($clientIdResponse) || is_null($clientIdEffectiveUrl) || is_null($clientIdMf2)) { + 614                            try { + 615                                /** @var ResponseInterface $clientIdResponse */ + 616                                list($clientIdResponse, $clientIdEffectiveUrl) = call_user_func($this->httpGetWithEffectiveUrl, $queryParams['client_id']); + 617                                $clientIdMf2 = Mf2\parse((string) $clientIdResponse->getBody(), $clientIdEffectiveUrl); + 618                            } catch (ClientExceptionInterface | RequestExceptionInterface | NetworkExceptionInterface $e) { + 619                                $this->logger->error("Caught an HTTP exception while trying to fetch the client_id. Returning an error response.", [ + 620                                    'client_id' => $queryParams['client_id'], + 621                                    'exception' => $e->__toString() + 622                                ]); + 623                                 + 624                                // At this point in the flow, we’ve already guaranteed that the redirect_uri is valid, + 625                                // so in theory we should report these errors by redirecting there. + 626                                throw IndieAuthException::create(IndieAuthException::INTERNAL_ERROR_REDIRECT, $request, $e); + 627                            } catch (Exception $e) { + 628                                $this->logger->error("Caught an unknown exception while trying to fetch the client_id. Returning an error response.", [ + 629                                    'exception' => $e->__toString() + 630                                ]); + 631     + 632                                throw IndieAuthException::create(IndieAuthException::INTERNAL_ERROR_REDIRECT, $request, $e); + 633                            } + 634                        } + 635                         + 636                        // Search for an h-app with u-url matching the client_id. + 637                        $clientHApps = M\findMicroformatsByProperty(M\findMicroformatsByType($clientIdMf2, 'h-app'), 'url', $queryParams['client_id']); + 638                        $clientHApp = empty($clientHApps) ? null : $clientHApps[0]; + 639 + 640                        // Present the authorization UI. + 641                        return $this->authorizationForm->showForm($request, $authenticationResult, $authenticationRedirect, $clientHApp) + 642                                ->withAddedHeader('Cache-control', 'no-cache'); + 643                    } + 644                } + 645 + 646                // If the request isn’t an IndieAuth Authorization or Code-redeeming request, it’s either an invalid + 647                // request or something to do with a custom auth handler (e.g. sending a one-time code in an email.) + 648                $nonIndieAuthRequestResult = call_user_func($this->handleNonIndieAuthRequest, $request); + 649                if ($nonIndieAuthRequestResult instanceof ResponseInterface) { + 650                    return $nonIndieAuthRequestResult; + 651                } else { + 652                    // In this code path we have not validated the redirect_uri, so show a regular error page + 653                    // rather than returning a redirect error. + 654                    throw IndieAuthException::create(IndieAuthException::INTERNAL_ERROR, $request); + 655                } + 656            } catch (IndieAuthException $e) { + 657                // All IndieAuthExceptions will already have been logged. + 658                return $this->handleException($e); + 659            } catch (Exception $e) { + 660                // Unknown exceptions will not have been logged; do so now. + 661                $this->logger->error("Caught unknown exception: {$e}"); + 662                return $this->handleException(IndieAuthException::create(0, $request, $e)); + 663            } + 664        }));     + 665    } + 666 + 667    /** + 668     * Handle Token Endpoint Request + 669     *  + 670     * Handles requests to the IndieAuth token endpoint. The logical flow can be summarised as follows: + 671     *  + 672     * * Check that the request is a code redeeming request. Return an error if not. + 673     * * Ensure that all required parameters are present. Return an error if not. + 674     * * Attempt to exchange the `code` parameter for an access token. Return an error if it fails. + 675     * * 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. + 676     * * 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. + 677     * * Make sure the granted scope stored in the auth code is not empty. If it is, revoke the access token and return an error. + 678     * * Otherwise, return a success response containing information about the issued access token. + 679     *  + 680     * This method must NOT be CSRF-protected as it accepts external requests from client apps. + 681     *  + 682     * @param ServerRequestInterface $request + 683     * @return ResponseInterface + 684     */ + 685    public function handleTokenEndpointRequest(ServerRequestInterface $request): ResponseInterface { + 686        if (isIndieAuthAuthorizationCodeRedeemingRequest($request)) { + 687            $this->logger->info('Handling a request to redeem an authorization code for profile information.'); + 688             + 689            $bodyParams = $request->getParsedBody(); + 690 + 691            if (!isset($bodyParams['code'])) { + 692                $this->logger->warning('The exchange request was missing the code parameter. Returning an error response.'); + 693                return new Response(400, ['content-type' => 'application/json'], json_encode([ + 694                    'error' => 'invalid_request', + 695                    'error_description' => 'The code parameter was missing.' + 696                ])); + 697            } + 698 + 699            // Attempt to internally exchange the provided auth code for an access token. + 700            // We do this before anything else so that the auth code is invalidated as soon as the request starts, + 701            // and the resulting access token is revoked if we encounter an error. This ends up providing a simpler + 702            // and more flexible interface for TokenStorage implementors. + 703            try { + 704                // Call the token exchange method, passing in a callback which performs additional validation + 705                // on the auth code before it gets exchanged. + 706                $tokenData = $this->tokenStorage->exchangeAuthCodeForAccessToken($bodyParams['code'], function (array $authCode) use ($request, $bodyParams) { + 707                    // Verify that all required parameters are included. + 708                    $requiredParameters = ['client_id', 'redirect_uri', 'code_verifier']; + 709                    $missingRequiredParameters = array_filter($requiredParameters, function ($p) use ($bodyParams) { + 710                        return !array_key_exists($p, $bodyParams) || empty($bodyParams[$p]); + 711                    }); + 712                    if (!empty($missingRequiredParameters)) { + 713                        $this->logger->warning('The exchange request was missing required parameters. Returning an error response.', ['missing' => $missingRequiredParameters]); + 714                        throw IndieAuthException::create(IndieAuthException::INVALID_REQUEST, $request); + 715                    } + 716 + 717                    // Verify that it was issued for the same client_id and redirect_uri + 718                    if ($authCode['client_id'] !== $bodyParams['client_id'] + 719                        || $authCode['redirect_uri'] !== $bodyParams['redirect_uri']) { + 720                        $this->logger->error("The provided client_id and/or redirect_uri did not match those stored in the token."); + 721                        throw IndieAuthException::create(IndieAuthException::INVALID_GRANT, $request); + 722                    } + 723 + 724                    // Check that the supplied code_verifier hashes to the stored code_challenge + 725                    // TODO: support method = plain as well as S256. + 726                    if (!hash_equals($authCode['code_challenge'], generatePKCECodeChallenge($bodyParams['code_verifier']))) { + 727                        $this->logger->error("The provided code_verifier did not hash to the stored code_challenge"); + 728                        throw IndieAuthException::create(IndieAuthException::INVALID_GRANT, $request); + 729                    } + 730 + 731                    // Check that scope is not empty. + 732                    if (empty($authCode['scope'])) { + 733                        $this->logger->error("An exchange request for a token with an empty scope was sent to the token endpoint."); + 734                        throw IndieAuthException::create(IndieAuthException::INVALID_GRANT, $request); + 735                    } + 736                }); + 737            } catch (IndieAuthException $e) { + 738                // If an exception was thrown, return a corresponding error response. + 739                return new Response(400, ['content-type' => 'application/json'], json_encode([ + 740                    'error' => $e->getInfo()['error'], + 741                    'error_description' => $e->getMessage() + 742                ])); + 743            } + 744 + 745            if (is_null($tokenData)) { + 746                $this->logger->error('Attempting to exchange an auth code for a token resulted in null.', $bodyParams); + 747                return new Response(400, ['content-type' => 'application/json'], json_encode([ + 748                    'error' => 'invalid_grant', + 749                    'error_description' => 'The provided credentials were not valid.' + 750                ])); + 751            } + 752 + 753            // TODO: return an error if the token doesn’t contain a me key. + 754 + 755            // If everything checked out, return {"me": "https://example.com"} response + 756            return new Response(200, [ + 757                'content-type' => 'application/json', + 758                'cache-control' => 'no-store', + 759            ], json_encode(array_merge([ + 760                // Ensure that the token_type key is present, if tokenStorage doesn’t include it. + 761                'token_type' => 'Bearer' + 762            ], array_filter($tokenData, function (string $k) { + 763                // We should be able to trust the return data from tokenStorage, but there’s no harm in + 764                // preventing code_challenges from leaking, per OAuth2. + 765                return !in_array($k, ['code_challenge', 'code_challenge_method']); + 766            }, ARRAY_FILTER_USE_KEY)))); + 767        } + 768 + 769        return new Response(400, ['content-type' => 'application/json'], json_encode([ + 770            'error' => 'invalid_request', + 771            'error_description' => 'Request to token endpoint was not a valid code exchange request.' + 772        ])); + 773    } + 774 + 775    /** + 776     * Handle Exception + 777     *  + 778     * Turns an instance of `IndieAuthException` into an appropriate instance of `ResponseInterface`. + 779     */ + 780    protected function handleException(IndieAuthException $exception): ResponseInterface { + 781        $exceptionData = $exception->getInfo(); + 782 + 783        if ($exceptionData['statusCode'] == 302) { + 784            // This exception is handled by redirecting to the redirect_uri with error parameters. + 785            $redirectQueryParams = [ + 786                'error' => $exceptionData['error'] ?? 'invalid_request', + 787                'error_description' => (string) $exception + 788            ]; 789 - 790            return new Response($exceptionData['statusCode'], [ - 791                'Location' => appendQueryParams((string) $exception->getRequest()->getQueryParams()['redirect_uri'], $redirectQueryParams) - 792            ]); - 793        } else { - 794            // This exception should be shown to the user. - 795            return new Response($exception->getStatusCode(), ['content-type' => 'text/html'], renderTemplate($this->exceptionTemplatePath, [ - 796                'request' => $exception->getRequest(), - 797                'exception' => $exception - 798            ])); - 799        } - 800    } - 801} + 790            // If the state parameter was valid, include it in the error redirect. + 791            if ($exception->getCode() !== IndieAuthException::INVALID_STATE) { + 792                $redirectQueryParams['state'] = $exception->getRequest()->getQueryParams()['state']; + 793            } + 794 + 795            return new Response($exceptionData['statusCode'], [ + 796                'Location' => appendQueryParams((string) $exception->getRequest()->getQueryParams()['redirect_uri'], $redirectQueryParams) + 797            ]); + 798        } else { + 799            // This exception should be shown to the user. + 800            return new Response($exception->getStatusCode(), ['content-type' => 'text/html'], renderTemplate($this->exceptionTemplatePath, [ + 801                'request' => $exception->getRequest(), + 802                'exception' => $exception + 803            ])); + 804        } + 805    } + 806} @@ -1021,7 +1026,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 Mon Jun 14 21:52:39 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 Mon Jun 14 23:14:13 UTC 2021.

diff --git a/docs/coverage/Storage/FilesystemJsonStorage.php.html b/docs/coverage/Storage/FilesystemJsonStorage.php.html index cb38abd..33a7b55 100644 --- a/docs/coverage/Storage/FilesystemJsonStorage.php.html +++ b/docs/coverage/Storage/FilesystemJsonStorage.php.html @@ -89,7 +89,7 @@
53.85%
7 / 13
- 48.01 + 46.96
92.31% covered (success) @@ -110,7 +110,7 @@
0.00%
0 / 1
- 4.10 + 3.05
81.82% covered (warning) @@ -421,7 +421,7 @@ 40    public function __construct(string $path, string $secret, ?int $authCodeTtl=null, ?int $accessTokenTtl=null, $cleanUpNow=false, ?LoggerInterface $logger=null) { 41        $this->logger = $logger ?? new NullLogger(); 42 - 43        if (!is_string($secret) || strlen($secret) < 64) { + 43        if (strlen($secret) < 64) { 44            throw new Exception("\$secret must be a string with a minimum length of 64 characters. Make one with Taproot\IndieAuth\generateRandomString(64)"); 45        } 46        $this->secret = $secret; @@ -663,7 +663,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 Mon Jun 14 21:52:39 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 Mon Jun 14 23:14:13 UTC 2021.

diff --git a/docs/coverage/Storage/Sqlite3Storage.php.html b/docs/coverage/Storage/Sqlite3Storage.php.html index b3343b5..cbd00c1 100644 --- a/docs/coverage/Storage/Sqlite3Storage.php.html +++ b/docs/coverage/Storage/Sqlite3Storage.php.html @@ -56,20 +56,6 @@
0 / 0
- - Sqlite3Storage - -
n/a
-
0 / 0
- -
n/a
-
0 / 0
- 0 - -
n/a
-
0 / 0
- - @@ -80,9 +66,9 @@
2 3namespace Taproot\IndieAuth\Storage; 4 - 5class Sqlite3Storage implements TokenStorageInterface { - 6     - 7} + 5/*class Sqlite3Storage implements TokenStorageInterface { + 6     + 7}*/ @@ -93,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 Mon Jun 14 21:52:39 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 Mon Jun 14 23:14:13 UTC 2021.

diff --git a/docs/coverage/Storage/TokenStorageInterface.php.html b/docs/coverage/Storage/TokenStorageInterface.php.html index 2cf202d..b780ef7 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 Mon Jun 14 21:52:39 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 Mon Jun 14 23:14:13 UTC 2021.

diff --git a/docs/coverage/Storage/dashboard.html b/docs/coverage/Storage/dashboard.html index 6b24899..5d0eb95 100644 --- a/docs/coverage/Storage/dashboard.html +++ b/docs/coverage/Storage/dashboard.html @@ -130,7 +130,7 @@ - __construct4 + __construct3 createAuthCode3 delete3 @@ -142,7 +142,7 @@
@@ -161,7 +161,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,1], "Class Coverage")) + .datum(getCoverageDistributionData([0,0,0,0,0,0,0,0,0,0,1,0], "Class Coverage")) .transition().duration(500).call(chart); nv.utils.windowResize(chart.update); @@ -229,7 +229,7 @@ $(document).ready(function() { chart.yAxis.axisLabel('Cyclomatic Complexity'); d3.select('#classComplexity svg') - .datum(getComplexityData([[92.3076923076923,47,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage<\/a>"],[100,0,"Taproot\\IndieAuth\\Storage\\Sqlite3Storage<\/a>"]], 'Class Complexity')) + .datum(getComplexityData([[92.3076923076923,46,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage<\/a>"]], 'Class Complexity')) .transition() .duration(500) .call(chart); @@ -253,7 +253,7 @@ $(document).ready(function() { chart.yAxis.axisLabel('Method Complexity'); d3.select('#methodComplexity svg') - .datum(getComplexityData([[81.81818181818183,4,"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>"],[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')) .transition() .duration(500) .call(chart); diff --git a/docs/coverage/Storage/index.html b/docs/coverage/Storage/index.html index 66c8647..33db78b 100644 --- a/docs/coverage/Storage/index.html +++ b/docs/coverage/Storage/index.html @@ -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 Mon Jun 14 21:52:39 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 Mon Jun 14 23:14:13 UTC 2021.

diff --git a/docs/coverage/dashboard.html b/docs/coverage/dashboard.html index 2d9b13c..f184e23 100644 --- a/docs/coverage/dashboard.html +++ b/docs/coverage/dashboard.html @@ -137,7 +137,7 @@ trustQueryParams6 - __construct4 + __construct3 createAuthCode3 delete3 create2 @@ -150,7 +150,7 @@
@@ -169,7 +169,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,5], "Class Coverage")) + .datum(getCoverageDistributionData([1,0,0,0,0,0,0,1,0,0,3,4], "Class Coverage")) .transition().duration(500).call(chart); nv.utils.windowResize(chart.update); @@ -237,7 +237,7 @@ $(document).ready(function() { chart.yAxis.axisLabel('Cyclomatic Complexity'); d3.select('#classComplexity svg') - .datum(getComplexityData([[100,6,"Taproot\\IndieAuth\\Callback\\DefaultAuthorizationForm<\/a>"],[100,6,"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>"],[94.90196078431372,83,"Taproot\\IndieAuth\\Server<\/a>"],[92.3076923076923,47,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage<\/a>"],[100,0,"Taproot\\IndieAuth\\Storage\\Sqlite3Storage<\/a>"]], 'Class Complexity')) + .datum(getComplexityData([[100,6,"Taproot\\IndieAuth\\Callback\\DefaultAuthorizationForm<\/a>"],[100,6,"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>"],[94.921875,86,"Taproot\\IndieAuth\\Server<\/a>"],[92.3076923076923,46,"Taproot\\IndieAuth\\Storage\\FilesystemJsonStorage<\/a>"]], 'Class Complexity')) .transition() .duration(500) .call(chart); @@ -261,7 +261,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,3,"Taproot\\IndieAuth\\Callback\\SingleUserPasswordAuthenticationCallback::__construct<\/a>"],[100,3,"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>"],[92.90322580645162,51,"Taproot\\IndieAuth\\Server::handleAuthorizationEndpointRequest<\/a>"],[95.34883720930233,11,"Taproot\\IndieAuth\\Server::handleTokenEndpointRequest<\/a>"],[100,3,"Taproot\\IndieAuth\\Server::handleException<\/a>"],[81.81818181818183,4,"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,3,"Taproot\\IndieAuth\\Callback\\SingleUserPasswordAuthenticationCallback::__construct<\/a>"],[100,3,"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>"],[92.94871794871796,54,"Taproot\\IndieAuth\\Server::handleAuthorizationEndpointRequest<\/a>"],[95.34883720930233,11,"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')) .transition() .duration(500) .call(chart); diff --git a/docs/coverage/functions.php.html b/docs/coverage/functions.php.html index 91a2bc8..655a844 100644 --- a/docs/coverage/functions.php.html +++ b/docs/coverage/functions.php.html @@ -129,7 +129,7 @@ - Taproot\IndieAuth\base64_urlencode + Taproot\IndieAuth\base64_urlencode
100.00% covered (success) @@ -171,7 +171,7 @@ - Taproot\IndieAuth\isIndieAuthAuthorizationCodeRedeemingRequest + Taproot\IndieAuth\isIndieAuthAuthorizationCodeRedeemingRequest
100.00% covered (success) @@ -192,7 +192,7 @@ - Taproot\IndieAuth\isIndieAuthAuthorizationRequest + Taproot\IndieAuth\isIndieAuthAuthorizationRequest
100.00% covered (success) @@ -213,7 +213,7 @@ - Taproot\IndieAuth\isAuthorizationApprovalRequest + Taproot\IndieAuth\isAuthorizationApprovalRequest
100.00% covered (success) @@ -234,7 +234,7 @@ - Taproot\IndieAuth\buildQueryString + Taproot\IndieAuth\buildQueryString
100.00% covered (success) @@ -255,7 +255,7 @@ - Taproot\IndieAuth\urlComponentsMatch + Taproot\IndieAuth\urlComponentsMatch
0.00% covered (danger) @@ -489,44 +489,44 @@ 38    return base64_urlencode(hash('sha256', $plaintext, true)); 39} 40 - 41function base64_urlencode($string) { + 41function base64_urlencode(string $string): string { 42    return rtrim(strtr(base64_encode($string), '+/', '-_'), '='); 43} 44 45function hashAuthorizationRequestParameters(ServerRequestInterface $request, string $secret, ?string $algo=null, ?array $hashedParameters=null): ?string { - 46    $hashedParameters = $hashedParameters ?? ['client_id', 'redirect_uri', 'code_challenge', 'code_challenge_method']; - 47    $algo = $algo ?? 'sha256'; + 46    $hashedParameters = $hashedParameters ?? ['client_id', 'redirect_uri', 'code_challenge', 'code_challenge_method']; + 47    $algo = $algo ?? 'sha256'; 48 - 49    $queryParams = $request->getQueryParams() ?? []; - 50    $data = ''; - 51    foreach ($hashedParameters as $key) { - 52        if (!array_key_exists($key, $queryParams)) { + 49    $queryParams = $request->getQueryParams(); + 50    $data = ''; + 51    foreach ($hashedParameters as $key) { + 52        if (!isset($queryParams[$key])) { 53            return null; 54        } - 55        $data .= $queryParams[$key]; + 55        $data .= $queryParams[$key]; 56    } - 57    return hash_hmac($algo, $data, $secret); + 57    return hash_hmac($algo, $data, $secret); 58} 59 - 60function isIndieAuthAuthorizationCodeRedeemingRequest(ServerRequestInterface $request) { + 60function isIndieAuthAuthorizationCodeRedeemingRequest(ServerRequestInterface $request): bool { 61    return strtolower($request->getMethod()) == 'post' 62            && array_key_exists('grant_type', $request->getParsedBody() ?? []) 63            && $request->getParsedBody()['grant_type'] == 'authorization_code'; 64} 65 - 66function isIndieAuthAuthorizationRequest(ServerRequestInterface $request, $permittedMethods=['get']) { + 66function isIndieAuthAuthorizationRequest(ServerRequestInterface $request, array $permittedMethods=['get']): bool { 67    return in_array(strtolower($request->getMethod()), array_map('strtolower', $permittedMethods)) 68            && array_key_exists('response_type', $request->getQueryParams() ?? []) 69            && $request->getQueryParams()['response_type'] == 'code'; 70} 71 - 72function isAuthorizationApprovalRequest(ServerRequestInterface $request) { + 72function isAuthorizationApprovalRequest(ServerRequestInterface $request): bool { 73    return strtolower($request->getMethod()) == 'post' 74            && array_key_exists('taproot_indieauth_action', $request->getParsedBody() ?? []) 75            && $request->getParsedBody()[Server::APPROVE_ACTION_KEY] == Server::APPROVE_ACTION_VALUE; 76} 77 - 78function buildQueryString(array $parameters) { + 78function buildQueryString(array $parameters): string { 79    $qs = []; 80    foreach ($parameters as $k => $v) { 81        $qs[] = urlencode($k) . '=' . urlencode($v); @@ -534,7 +534,7 @@ 83    return join('&', $qs); 84} 85 - 86function urlComponentsMatch($url1, $url2, ?array $components=null): bool { + 86function urlComponentsMatch(string $url1, string $url2, ?array $components=null): bool { 87    $validComponents = [PHP_URL_HOST, PHP_URL_PASS, PHP_URL_PATH, PHP_URL_PORT, PHP_URL_USER, PHP_URL_QUERY, PHP_URL_SCHEME, PHP_URL_FRAGMENT]; 88    $components = $components ?? $validComponents; 89 @@ -680,7 +680,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 Mon Jun 14 21:52:39 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 Mon Jun 14 23:14:13 UTC 2021.

diff --git a/docs/coverage/index.html b/docs/coverage/index.html index 24636ba..4b0a5d0 100644 --- a/docs/coverage/index.html +++ b/docs/coverage/index.html @@ -44,13 +44,13 @@ Total
-
- 94.58% covered (success) +
+ 94.59% covered (success)
-
94.58%
-
506 / 535
+
94.59%
+
507 / 536
73.68% covered (warning) @@ -184,13 +184,13 @@ Server.php
-
- 94.90% covered (success) +
+ 94.92% covered (success)
-
94.90%
-
242 / 255
+
94.92%
+
243 / 256
60.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 Mon Jun 14 21:52:39 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 Mon Jun 14 23:14:13 UTC 2021.

diff --git a/docs/files/src-functions.html b/docs/files/src-functions.html index 2622513..30eecc4 100644 --- a/docs/files/src-functions.html +++ b/docs/files/src-functions.html @@ -112,7 +112,7 @@
base64_urlencode() -  : mixed +  : string
@@ -126,28 +126,28 @@
isIndieAuthAuthorizationCodeRedeemingRequest() -  : mixed +  : bool
isIndieAuthAuthorizationRequest() -  : mixed +  : bool
isAuthorizationApprovalRequest() -  : mixed +  : bool
buildQueryString() -  : mixed +  : string
@@ -334,14 +334,14 @@ - base64_urlencode(mixed $string) : mixed + base64_urlencode(string $string) : string
Parameters
$string - : mixed + : string
@@ -419,7 +419,7 @@ - isIndieAuthAuthorizationCodeRedeemingRequest(ServerRequestInterface $request) : mixed + isIndieAuthAuthorizationCodeRedeemingRequest(ServerRequestInterface $request) : bool
Parameters
@@ -451,7 +451,7 @@ - isIndieAuthAuthorizationRequest(ServerRequestInterface $request[, mixed $permittedMethods = ['get'] ]) : mixed + isIndieAuthAuthorizationRequest(ServerRequestInterface $request[, array<string|int, mixed> $permittedMethods = ['get'] ]) : bool
Parameters
@@ -465,7 +465,7 @@
$permittedMethods - : mixed + : array<string|int, mixed> = ['get']
@@ -490,7 +490,7 @@ - isAuthorizationApprovalRequest(ServerRequestInterface $request) : mixed + isAuthorizationApprovalRequest(ServerRequestInterface $request) : bool
Parameters
@@ -522,7 +522,7 @@ - buildQueryString(array<string|int, mixed> $parameters) : mixed + buildQueryString(array<string|int, mixed> $parameters) : string
Parameters
@@ -554,21 +554,21 @@ - urlComponentsMatch(mixed $url1, mixed $url2[, array<string|int, mixed>|null $components = null ]) : bool + urlComponentsMatch(string $url1, string $url2[, array<string|int, mixed>|null $components = null ]) : bool
Parameters
$url1 - : mixed + : string
$url2 - : mixed + : string
diff --git a/docs/files/src-storage-sqlite3storage.html b/docs/files/src-storage-sqlite3storage.html index b32948c..6eb5fb8 100644 --- a/docs/files/src-storage-sqlite3storage.html +++ b/docs/files/src-storage-sqlite3storage.html @@ -80,17 +80,6 @@ -

- Interfaces, Classes and Traits - -

- -
- -
Sqlite3Storage
-
- -
diff --git a/docs/js/searchIndex.js b/docs/js/searchIndex.js index bcefdfb..6f447d6 100644 --- a/docs/js/searchIndex.js +++ b/docs/js/searchIndex.js @@ -645,11 +645,6 @@ Search.appendIndex( "name": "logger", "summary": "", "url": "classes/Taproot-IndieAuth-Storage-FilesystemJsonStorage.html#property_logger" - }, { - "fqsen": "\\Taproot\\IndieAuth\\Storage\\Sqlite3Storage", - "name": "Sqlite3Storage", - "summary": "", - "url": "classes/Taproot-IndieAuth-Storage-Sqlite3Storage.html" }, { "fqsen": "\\Taproot\\IndieAuth\\Storage\\TokenStorageInterface", "name": "TokenStorageInterface", diff --git a/docs/namespaces/taproot-indieauth-storage.html b/docs/namespaces/taproot-indieauth-storage.html index 5f27424..5671b2b 100644 --- a/docs/namespaces/taproot-indieauth-storage.html +++ b/docs/namespaces/taproot-indieauth-storage.html @@ -93,8 +93,6 @@
FilesystemJsonStorage
Filesystem JSON Token Storage
-
Sqlite3Storage
-
diff --git a/docs/namespaces/taproot-indieauth.html b/docs/namespaces/taproot-indieauth.html index bc47bea..b95d5be 100644 --- a/docs/namespaces/taproot-indieauth.html +++ b/docs/namespaces/taproot-indieauth.html @@ -136,7 +136,7 @@
base64_urlencode() -  : mixed +  : string
@@ -150,28 +150,28 @@
isIndieAuthAuthorizationCodeRedeemingRequest() -  : mixed +  : bool
isIndieAuthAuthorizationRequest() -  : mixed +  : bool
isAuthorizationApprovalRequest() -  : mixed +  : bool
buildQueryString() -  : mixed +  : string
@@ -357,14 +357,14 @@ - base64_urlencode(mixed $string) : mixed + base64_urlencode(string $string) : string
Parameters
$string - : mixed + : string
@@ -442,7 +442,7 @@ - isIndieAuthAuthorizationCodeRedeemingRequest(ServerRequestInterface $request) : mixed + isIndieAuthAuthorizationCodeRedeemingRequest(ServerRequestInterface $request) : bool
Parameters
@@ -474,7 +474,7 @@ - isIndieAuthAuthorizationRequest(ServerRequestInterface $request[, mixed $permittedMethods = ['get'] ]) : mixed + isIndieAuthAuthorizationRequest(ServerRequestInterface $request[, array<string|int, mixed> $permittedMethods = ['get'] ]) : bool
Parameters
@@ -488,7 +488,7 @@
$permittedMethods - : mixed + : array<string|int, mixed> = ['get']
@@ -513,7 +513,7 @@ - isAuthorizationApprovalRequest(ServerRequestInterface $request) : mixed + isAuthorizationApprovalRequest(ServerRequestInterface $request) : bool
Parameters
@@ -545,7 +545,7 @@ - buildQueryString(array<string|int, mixed> $parameters) : mixed + buildQueryString(array<string|int, mixed> $parameters) : string
Parameters
@@ -577,21 +577,21 @@ - urlComponentsMatch(mixed $url1, mixed $url2[, array<string|int, mixed>|null $components = null ]) : bool + urlComponentsMatch(string $url1, string $url2[, array<string|int, mixed>|null $components = null ]) : bool
Parameters
$url1 - : mixed + : string
$url2 - : mixed + : string
diff --git a/docs/packages/default.html b/docs/packages/default.html index 3fcd155..5c2e1ad 100644 --- a/docs/packages/default.html +++ b/docs/packages/default.html @@ -109,8 +109,6 @@
IndieAuth Server
FilesystemJsonStorage
Filesystem JSON Token Storage
-
Sqlite3Storage
-
@@ -145,7 +143,7 @@
base64_urlencode() -  : mixed +  : string
@@ -159,28 +157,28 @@
isIndieAuthAuthorizationCodeRedeemingRequest() -  : mixed +  : bool
isIndieAuthAuthorizationRequest() -  : mixed +  : bool
isAuthorizationApprovalRequest() -  : mixed +  : bool
buildQueryString() -  : mixed +  : string
@@ -366,14 +364,14 @@ - base64_urlencode(mixed $string) : mixed + base64_urlencode(string $string) : string
Parameters
$string - : mixed + : string
@@ -451,7 +449,7 @@ - isIndieAuthAuthorizationCodeRedeemingRequest(ServerRequestInterface $request) : mixed + isIndieAuthAuthorizationCodeRedeemingRequest(ServerRequestInterface $request) : bool
Parameters
@@ -483,7 +481,7 @@ - isIndieAuthAuthorizationRequest(ServerRequestInterface $request[, mixed $permittedMethods = ['get'] ]) : mixed + isIndieAuthAuthorizationRequest(ServerRequestInterface $request[, array<string|int, mixed> $permittedMethods = ['get'] ]) : bool
Parameters
@@ -497,7 +495,7 @@
$permittedMethods - : mixed + : array<string|int, mixed> = ['get']
@@ -522,7 +520,7 @@ - isAuthorizationApprovalRequest(ServerRequestInterface $request) : mixed + isAuthorizationApprovalRequest(ServerRequestInterface $request) : bool
Parameters
@@ -554,7 +552,7 @@ - buildQueryString(array<string|int, mixed> $parameters) : mixed + buildQueryString(array<string|int, mixed> $parameters) : string
Parameters
@@ -586,21 +584,21 @@ - urlComponentsMatch(mixed $url1, mixed $url2[, array<string|int, mixed>|null $components = null ]) : bool + urlComponentsMatch(string $url1, string $url2[, array<string|int, mixed>|null $components = null ]) : bool
Parameters
$url1 - : mixed + : string
$url2 - : mixed + : string
diff --git a/src/Server.php b/src/Server.php index f37369d..cd3447b 100644 --- a/src/Server.php +++ b/src/Server.php @@ -442,7 +442,8 @@ class Server { // order to know that, we need to also validate, fetch and parse the client_id. // If the request lacks a hash, or if the provided hash was invalid, perform the validation. $currentRequestHash = hashAuthorizationRequestParameters($request, $this->secret); - if (is_null($currentRequestHash) or !hash_equals($currentRequestHash, $queryParams[self::HASH_QUERY_STRING_KEY])) { + if (!isset($queryParams[self::HASH_QUERY_STRING_KEY]) or is_null($currentRequestHash) or !hash_equals($currentRequestHash, $queryParams[self::HASH_QUERY_STRING_KEY])) { + // All we need to know at this stage is whether the redirect_uri is valid. If it // sufficiently matches the client_id, we don’t (yet) need to fetch the client_id. if (!urlComponentsMatch($queryParams['client_id'], $queryParams['redirect_uri'], [PHP_URL_SCHEME, PHP_URL_HOST, PHP_URL_PORT])) { @@ -561,7 +562,7 @@ class Server { } $expectedHash = hashAuthorizationRequestParameters($request, $this->secret); - if (is_null($expectedHash) or !hash_equals($expectedHash, $queryParams[self::HASH_QUERY_STRING_KEY])) { + if (!isset($queryParams[self::HASH_QUERY_STRING_KEY]) or is_null($expectedHash) or !hash_equals($expectedHash, $queryParams[self::HASH_QUERY_STRING_KEY])) { $this->logger->warning("The hash provided in the URL was invalid!", [ 'expected' => $expectedHash, 'actual' => $queryParams[self::HASH_QUERY_STRING_KEY] diff --git a/src/functions.php b/src/functions.php index 79fdb52..506b221 100644 --- a/src/functions.php +++ b/src/functions.php @@ -38,7 +38,7 @@ function generatePKCECodeChallenge($plaintext) { return base64_urlencode(hash('sha256', $plaintext, true)); } -function base64_urlencode($string) { +function base64_urlencode(string $string): string { return rtrim(strtr(base64_encode($string), '+/', '-_'), '='); } @@ -49,7 +49,7 @@ function hashAuthorizationRequestParameters(ServerRequestInterface $request, str $queryParams = $request->getQueryParams(); $data = ''; foreach ($hashedParameters as $key) { - if (!array_key_exists($key, $queryParams)) { + if (!isset($queryParams[$key])) { return null; } $data .= $queryParams[$key];