diff --git a/composer.json b/composer.json index 90d6fcf..bdb5bca 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,10 @@ ], "type": "library", "require": { - "php": ">=7.4 <=8.4.1" + "php": ">=7.4 <=8.4.1", + "ext-json": "*", + "ext-openssl": "*", + "ext-curl": "*" }, "license": "MIT", "autoload": { @@ -39,7 +42,8 @@ "phpunit/phpunit": "^9.6", "slevomat/coding-standard": "^8.4", "squizlabs/php_codesniffer": "^3.11", - "vimeo/psalm": "^4.9" + "vimeo/psalm": "^4.9", + "friendsofphp/php-cs-fixer": "^3.65" }, "scripts": { "analyze": [ diff --git a/src/Config/ConfigInterface.php b/src/Config/ConfigInterface.php index d392284..921d25b 100644 --- a/src/Config/ConfigInterface.php +++ b/src/Config/ConfigInterface.php @@ -6,10 +6,15 @@ interface ConfigInterface { public function getProduction(): bool; public function getSameSiteStrict(): ?bool; + /** @return array|null */ public function getError(): ?array; + /** @return array|null */ public function getScope(): ?array; + /** @return array|null */ public function getProviderHint(): ?array; + /** @return array */ public function getRoutes(): array; + /** @return array */ public function getCookies(): array; public function getLoginSync(): ?callable; public function getLogoutSync(): ?callable; diff --git a/src/Config/Constants.php b/src/Config/Constants.php index 0aa3c28..23a7432 100644 --- a/src/Config/Constants.php +++ b/src/Config/Constants.php @@ -7,22 +7,30 @@ class Constants public static string $PRODUCTION_WALLET = 'https://wallet.hello.coop'; public static string $DEFAULT_PATH = '/authorize'; public static string $HELLO_API_ROUTE = '/api/hellocoop'; + /** @var array */ public static array $DEFAULT_SCOPE = ['openid', 'name', 'email', 'picture']; public static string $DEFAULT_RESPONSE_TYPE = 'code'; public static string $DEFAULT_RESPONSE_MODE = 'query'; + /** @var array */ public static array $VALID_IDENTITY_STRING_CLAIMS = [ 'name', 'nickname', 'preferred_username', 'given_name', 'family_name', 'email', 'phone', 'picture', 'ethereum', ]; + /** + * @var array + */ public static array $VALID_IDENTITY_ACCOUNT_CLAIMS = [ 'discord', 'twitter', 'github', 'gitlab' ]; public static string $ORG_CLAIM = 'org'; - public static function getValidIdentityClaims() + /** + * @return array + */ + public static function getValidIdentityClaims(): array { return array_merge( self::$VALID_IDENTITY_STRING_CLAIMS, @@ -31,7 +39,10 @@ public static function getValidIdentityClaims() ); } - public static function getValidScopes() + /** + * @return array + */ + public static function getValidScopes(): array { return array_merge( self::$VALID_IDENTITY_STRING_CLAIMS, @@ -40,9 +51,11 @@ public static function getValidScopes() ); } + /** @var array */ public static array $VALID_RESPONSE_TYPE = ['id_token', 'code']; + /** @var array */ public static array $VALID_RESPONSE_MODE = ['fragment', 'query', 'form_post']; - + /** @var array */ public static array $VALID_PROVIDER_HINT = [ 'apple', 'discord', 'facebook', 'github', 'gitlab', 'google', 'twitch', 'twitter', 'tumblr', 'mastodon', 'microsoft', 'line', diff --git a/src/Config/HelloConfig.php b/src/Config/HelloConfig.php index 4710484..fd04895 100644 --- a/src/Config/HelloConfig.php +++ b/src/Config/HelloConfig.php @@ -17,15 +17,45 @@ class HelloConfig implements ConfigInterface private string $host; private ?string $secret = null; private ?bool $logDebug = null; + /** @var array|null */ private ?array $error = null; + /** @var array */ private array $scope; + /** @var array */ private array $providerHint; + /** @var array */ private array $routes; + /** @var array */ private array $cookies; + /** @var callable|null */ private $loginSync; + /** @var callable|null */ private $logoutSync; private bool $production; + /** + * @param string $apiRoute + * @param string $authApiRoute + * @param string $loginApiRoute + * @param string $logoutApiRoute + * @param bool $sameSiteStrict + * @param string|null $clientId + * @param string|null $redirectURI + * @param string $host + * @param string|null $secret + * @param callable|null $loginSync + * @param callable|null $logoutSync + * @param array $cookies + * @param bool $production + * @param string $helloDomain + * @param string|null $helloWallet + * @param array $scope + * @param array $providerHint + * @param array $routes + * @param bool|null $cookieToken + * @param bool|null $logDebug + * @param array|null $error + */ public function __construct( string $apiRoute, string $authApiRoute, @@ -89,26 +119,41 @@ public function getSameSiteStrict(): ?bool return $this->sameSiteStrict; } + /** + * @return array|null + */ public function getError(): ?array { return $this->error; } + /** + * @return array|null + */ public function getScope(): ?array { return $this->scope; } + /** + * @return array|null + */ public function getProviderHint(): ?array { return $this->providerHint; } + /** + * @return array + */ public function getRoutes(): array { return $this->routes; } + /** + * @return array + */ public function getCookies(): array { return $this->cookies; diff --git a/src/Config/HelloConfigBuilder.php b/src/Config/HelloConfigBuilder.php index 010270c..1266a4d 100644 --- a/src/Config/HelloConfigBuilder.php +++ b/src/Config/HelloConfigBuilder.php @@ -13,6 +13,7 @@ class HelloConfigBuilder private ?string $redirectURI = null; private string $host = ''; private ?string $secret = null; + /** @var array */ private array $cookies = [ 'authName' => 'hellocoop_auth', 'oidcName' => 'hellocoop_oidc', @@ -20,17 +21,23 @@ class HelloConfigBuilder private bool $production = true; private string $helloDomain = 'hello.coop'; private ?string $helloWallet = null; + /** @var array */ private array $scope = ['openid', 'name', 'email', 'picture']; + /** @var array */ private array $providerHint = ['github']; + /** @var array */ private array $routes = [ 'loggedIn' => '/', 'loggedOut' => '/', 'error' => '/error', ]; + /** @var callable|null */ private $loginSync = null; + /** @var callable|null */ private $logoutSync = null; private ?bool $cookieToken = null; private ?bool $logDebug = null; + /** @var array|null */ private ?array $error = null; public function setApiRoute(string $apiRoute): self @@ -87,6 +94,9 @@ public function setSecret(?string $secret): self return $this; } + /** + * @param array $cookies + */ public function setCookies(array $cookies): self { $this->cookies = $cookies; @@ -111,30 +121,47 @@ public function setHelloWallet(?string $helloWallet): self return $this; } + /** + * @param array $scope + */ public function setScope(array $scope): self { $this->scope = $scope; return $this; } + /** + * @param array $providerHint + */ public function setProviderHint(array $providerHint): self { $this->providerHint = $providerHint; return $this; } + /** + * @param array $routes + */ public function setRoutes(array $routes): self { $this->routes = $routes; return $this; } + /** + * @param callable|null $loginSync + * @return HelloConfigBuilder + */ public function setLoginSync(?callable $loginSync): self { $this->loginSync = $loginSync; return $this; } + /** + * @param callable|null $logoutSync + * @return HelloConfigBuilder + */ public function setLogoutSync(?callable $logoutSync): self { $this->logoutSync = $logoutSync; @@ -153,6 +180,9 @@ public function setLogDebug(?bool $logDebug): self return $this; } + /** + * @param array|null $error + */ public function setError(?array $error): self { $this->error = $error; diff --git a/src/Exception/CallbackException.php b/src/Exception/CallbackException.php index b5c3d65..840e6a7 100644 --- a/src/Exception/CallbackException.php +++ b/src/Exception/CallbackException.php @@ -7,8 +7,15 @@ class CallbackException extends Exception { + /** @var array */ private array $errorDetails; + /** + * @param array $errorDetails + * @param string $message + * @param int $code + * @param Throwable|null $previous + */ public function __construct( array $errorDetails, $message = "Callback Exception", @@ -19,6 +26,9 @@ public function __construct( parent::__construct($message, $code, $previous); } + /** + * @return array + */ public function getErrorDetails(): array { return $this->errorDetails; diff --git a/src/Exception/CryptoFailedException.php b/src/Exception/CryptoFailedException.php index 63e8f16..b70c991 100644 --- a/src/Exception/CryptoFailedException.php +++ b/src/Exception/CryptoFailedException.php @@ -6,5 +6,6 @@ class CryptoFailedException extends Exception { + /** @var string */ protected $message = 'Crypto failed. There was an error encrypting the data.'; } diff --git a/src/Exception/DecryptionFailedException.php b/src/Exception/DecryptionFailedException.php index c9a7641..4dbaa51 100644 --- a/src/Exception/DecryptionFailedException.php +++ b/src/Exception/DecryptionFailedException.php @@ -6,5 +6,6 @@ class DecryptionFailedException extends Exception { + /** @var string */ protected $message = 'Decryption failed. The data may be corrupted or the wrong key was used.'; } diff --git a/src/Exception/InvalidSecretException.php b/src/Exception/InvalidSecretException.php index 015018b..57bac4b 100644 --- a/src/Exception/InvalidSecretException.php +++ b/src/Exception/InvalidSecretException.php @@ -6,5 +6,6 @@ class InvalidSecretException extends Exception { + /** @var string */ protected $message = 'Invalid secret key. Must be a 64-character hexadecimal string.'; } diff --git a/src/Exception/NotImplementedException.php b/src/Exception/NotImplementedException.php index ff5866d..d5a7679 100644 --- a/src/Exception/NotImplementedException.php +++ b/src/Exception/NotImplementedException.php @@ -6,4 +6,6 @@ class NotImplementedException extends BadMethodCallException { + /** @var string */ + protected $message = 'Not Implemented.'; } diff --git a/src/Exception/SameSiteCallbackException.php b/src/Exception/SameSiteCallbackException.php index 2930bf6..d72cba5 100644 --- a/src/Exception/SameSiteCallbackException.php +++ b/src/Exception/SameSiteCallbackException.php @@ -7,8 +7,11 @@ class SameSiteCallbackException extends Exception { - private array $errorDetails; - + /** + * @param string $message + * @param int $code + * @param Throwable|null $previous + */ public function __construct( $message = "Same Site Callback Exception", $code = 0, diff --git a/src/Handler/Auth.php b/src/Handler/Auth.php index 497ee70..8a28473 100644 --- a/src/Handler/Auth.php +++ b/src/Handler/Auth.php @@ -15,6 +15,7 @@ class Auth private HelloRequestInterface $helloRequest; private ConfigInterface $config; private ?AuthLib $authLib = null; + public function __construct( HelloRequestInterface $helloRequest, HelloResponseInterface $helloResponse, @@ -38,6 +39,7 @@ public function handleAuth(): ?AuthType { return $this->getAuthLib()->getAuthfromCookies(); } + public function updateAuth(AuthUpdates $authUpdates): ?AuthType { $auth = $this->getAuthLib()->getAuthfromCookies(); @@ -48,6 +50,7 @@ public function updateAuth(AuthUpdates $authUpdates): ?AuthType $updatedAuth = array_merge($auth->toArray(), $authUpdates->toArray()); return AuthType::fromArray($updatedAuth); } + public function clearAuth(): void { $this->getAuthLib()->clearAuthCookie(); diff --git a/src/Handler/Callback.php b/src/Handler/Callback.php index da2b85a..b7b7f1f 100644 --- a/src/Handler/Callback.php +++ b/src/Handler/Callback.php @@ -2,6 +2,7 @@ namespace HelloCoop\Handler; +use HelloCoop\Exception\InvalidSecretException; use HelloCoop\HelloResponse\HelloResponseInterface; use HelloCoop\HelloRequest\HelloRequestInterface; use HelloCoop\Config\ConfigInterface; @@ -38,6 +39,9 @@ public function __construct( $this->config = $config; } + /** + * @throws InvalidSecretException + */ private function getOIDCManager(): OIDCManager { return $this->oidcManager ??= new OIDCManager( @@ -61,6 +65,7 @@ private function getTokenFetcher(): TokenFetcher { return $this->tokenFetcher ??= new TokenFetcher(new CurlWrapper()); } + private function getTokenParser(): TokenParser { return $this->tokenParser ??= new TokenParser(); @@ -87,7 +92,8 @@ public function handleCallback(): ?string throw new SameSiteCallbackException(); } - $oidcState = $this->getOIDCManager()->getOidc()->toArray(); + $oidc = $this->getOIDCManager()->getOidc(); + $oidcState = ($oidc) ? $oidc->toArray() : []; if (!$oidcState) { return $this->sendErrorPage([ 'error' => 'invalid_request', @@ -132,7 +138,7 @@ public function handleCallback(): ?string $this->getOIDCManager()->clearOidcCookie(); $token = $this->getTokenFetcher()->fetchToken([ - 'code' => (string) $code, + 'code' => $code, 'wallet' => $this->config->getHelloWallet(), 'code_verifier' => $codeVerifier, 'redirect_uri' => $redirectUri, @@ -189,7 +195,7 @@ public function handleCallback(): ?string } } - if ($auth['isLoggedIn'] && isset($payload['org'])) { + if (isset($payload['org'])) { $auth['authCookie']['org'] = $payload['org']; } @@ -259,7 +265,7 @@ public function handleCallback(): ?string * Uses the target URI from error details or a fallback error route. Updates the query * string with error information. Throws an exception if no error URI is available. * - * @param array $error Error details including 'target_uri', 'error', and 'error_description'. + * @param array $error Error details including 'target_uri', 'error', and 'error_description'. * @param string $errorMessage A message describing the error. * @param Throwable|null $previous Previous exception for chaining (optional). * @@ -271,7 +277,7 @@ private function sendErrorPage(array $error, string $errorMessage, ?Throwable $p { $error_uri = $error['target_uri'] ?? $this->config->getRoutes()['error'] ?? null; if ($error_uri) { - list($pathString, $queryString) = array_pad(explode('?', $error_uri, 2), 2, ''); + list($pathString, $queryString) = array_pad(explode('?', (string) $error_uri, 2), 2, ''); // Parse the query string into an array parse_str($queryString, $queryArray); foreach ($error as $key => $value) { diff --git a/src/Handler/Invite.php b/src/Handler/Invite.php index 182695a..7a697ca 100644 --- a/src/Handler/Invite.php +++ b/src/Handler/Invite.php @@ -2,6 +2,7 @@ namespace HelloCoop\Handler; +use Exception; use HelloCoop\HelloResponse\HelloResponseInterface; use HelloCoop\HelloRequest\HelloRequestInterface; use HelloCoop\Config\ConfigInterface; @@ -13,6 +14,7 @@ class Invite private HelloRequestInterface $helloRequest; private ConfigInterface $config; private ?AuthLib $authLib = null; + public function __construct( HelloRequestInterface $helloRequest, HelloResponseInterface $helloResponse, @@ -32,6 +34,9 @@ private function getAuthLib(): AuthLib ); } + /** + * @throws Exception + */ public function generateInviteUrl(): string { $params = $this->helloRequest->fetchMultiple([ @@ -46,7 +51,11 @@ public function generateInviteUrl(): string $auth = $this->getAuthLib()->getAuthfromCookies(); if (empty($auth->toArray()['authCookie'])) { - throw new \Exception("User not logged in"); + throw new Exception("User not logged in"); + } + + if (empty($auth->toArray()['authCookie']['sub'])) { + throw new Exception("User coookie missing"); } $request = [ @@ -62,7 +71,6 @@ public function generateInviteUrl(): string ]; $queryString = http_build_query($request); - $url = "https://wallet.{$this->config->getHelloDomain()}/invite?" . $queryString; - return $url; + return "https://wallet.{$this->config->getHelloDomain()}/invite?" . $queryString; } } diff --git a/src/Handler/Login.php b/src/Handler/Login.php index 7834ee3..753da61 100644 --- a/src/Handler/Login.php +++ b/src/Handler/Login.php @@ -2,6 +2,8 @@ namespace HelloCoop\Handler; +use HelloCoop\Exception\CryptoFailedException; +use HelloCoop\Exception\InvalidSecretException; use HelloCoop\HelloResponse\HelloResponseInterface; use HelloCoop\HelloRequest\HelloRequestInterface; use HelloCoop\Config\ConfigInterface; @@ -20,8 +22,15 @@ class Login private OIDCManager $oidcManager; private AuthHelper $authHelper; + /** @var array */ private array $redirectURIs; + /** + * @param HelloRequestInterface $helloRequest + * @param HelloResponseInterface $helloResponse + * @param ConfigInterface $config + * @param array $redirectURIs + */ public function __construct( HelloRequestInterface $helloRequest, HelloResponseInterface $helloResponse, @@ -34,6 +43,9 @@ public function __construct( $this->redirectURIs = $redirectURIs; } + /** + * @throws InvalidSecretException + */ private function getOIDCManager(): OIDCManager { return $this->oidcManager ??= new OIDCManager( @@ -49,6 +61,9 @@ private function getAuthHelper(): AuthHelper return $this->authHelper ??= new AuthHelper(new PKCE()); } + /** + * @throws CryptoFailedException|InvalidSecretException + */ public function generateLoginUrl(): ?string { $params = $this->helloRequest->fetchMultiple([ @@ -112,6 +127,6 @@ public function generateLoginUrl(): ?string 'target_uri' => $params['target_uri'], ])); - return $authResponse['url']; + return is_string($authResponse['url']) ? $authResponse['url'] : null; } } diff --git a/src/HelloClient.php b/src/HelloClient.php index 554d562..2465754 100644 --- a/src/HelloClient.php +++ b/src/HelloClient.php @@ -2,6 +2,9 @@ namespace HelloCoop; +use Exception; +use HelloCoop\Exception\CryptoFailedException; +use HelloCoop\Exception\InvalidSecretException; use HelloCoop\HelloRequest\HelloRequestInterface; use HelloCoop\HelloRequest\HelloRequest; use HelloCoop\HelloResponse\HelloResponseInterface; @@ -20,9 +23,9 @@ class HelloClient { private ConfigInterface $config; - private ?HelloResponseInterface $helloResponse; - private ?HelloRequestInterface $helloRequest; - private ?PageRendererInterface $pageRenderer; + private HelloResponseInterface $helloResponse; + private HelloRequestInterface $helloRequest; + private PageRendererInterface $pageRenderer; private ?Callback $callbackHandler = null; private ?Auth $authHandler = null; private ?Invite $invite = null; @@ -87,11 +90,18 @@ private function getLoginHandler(): Login ); } + /** + * @return array|null> + */ public function getAuth(): array { - return $this->getAuthHandler()->handleAuth()->toArray(); + return $this->getAuthHandler()->handleAuth() ? $this->getAuthHandler()->handleAuth()->toArray() : []; } + /** + * @throws InvalidSecretException + * @throws CryptoFailedException + */ private function handleLogin() { return $this->helloResponse->redirect($this->getLoginHandler()->generateLoginUrl()); @@ -102,6 +112,9 @@ private function handleLogout() return $this->helloResponse->redirect($this->getLogoutHandler()->generateLogoutUrl()); } + /** + * @throws Exception + */ private function handleInvite() { return $this->helloResponse->redirect($this->getInviteHandler()->generateInviteUrl()); @@ -150,7 +163,7 @@ public function route() case 'invite': return $this->handleInvite(); default: - throw new \Exception('unknown query: ' . $op); + throw new Exception('unknown query: ' . $op); //TODO: add 500 error here; } } diff --git a/src/HelloRequest/HelloRequest.php b/src/HelloRequest/HelloRequest.php index 9f9fe37..3630083 100644 --- a/src/HelloRequest/HelloRequest.php +++ b/src/HelloRequest/HelloRequest.php @@ -10,10 +10,10 @@ class HelloRequest implements HelloRequestInterface * Fetch a parameter by key from either GET or POST data. * * @param string $key The key of the parameter to fetch. - * @param mixed $default Default value if the key is not found. - * @return mixed The value of the parameter or default. + * @param string|null $default Default value if the key is not found. + * @return string|null The value of the parameter or default. */ - public function fetch(string $key, $default = null): ?string + public function fetch(string $key, string $default = null): ?string { // First check GET, then POST if not found. return $_GET[$key] ?? $_POST[$key] ?? $default; @@ -22,8 +22,8 @@ public function fetch(string $key, $default = null): ?string /** * Fetch multiple parameters by keys from either GET or POST data. * - * @param array $keys The keys of the parameters to fetch. - * @return array An associative array of parameters. + * @param array $keys The keys of the parameters to fetch. + * @return array An associative array of parameters. */ public function fetchMultiple(array $keys): array { @@ -38,16 +38,16 @@ public function fetchMultiple(array $keys): array * Fetch a header by key from the request headers. * * @param string $key The key of the header to fetch. - * @param mixed $default Default value if the key is not found. - * @return mixed The value of the header or default. + * @param string|null $default Default value if the key is not found. + * @return string|null The value of the header or default. */ - public function fetchHeader(string $key, $default = null): ?string + public function fetchHeader(string $key, string $default = null): ?string { $headers = $this->getAllHeaders(); $normalizedKey = strtolower($key); - foreach ($headers as $headerKey => $value) { + foreach ($headers as $headerKey => $headerValue) { if (strtolower($headerKey) === $normalizedKey) { - return $value; + return $headerValue; } } return $default; @@ -56,7 +56,7 @@ public function fetchHeader(string $key, $default = null): ?string /** * Retrieve all request headers. * - * @return array An associative array of all request headers. + * @return array An associative array of all request headers. */ private function getAllHeaders(): array { diff --git a/src/HelloRequest/HelloRequestInterface.php b/src/HelloRequest/HelloRequestInterface.php index b0a5493..f073b83 100644 --- a/src/HelloRequest/HelloRequestInterface.php +++ b/src/HelloRequest/HelloRequestInterface.php @@ -8,16 +8,16 @@ interface HelloRequestInterface * Fetch a parameter by key from either GET or POST data. * * @param string $key The key of the parameter to fetch. - * @param mixed $default Default value if the key is not found. - * @return mixed The value of the parameter or default. + * @param string|null $default Default value if the key is not found. + * @return string|null The value of the parameter or default. */ - public function fetch(string $key, $default = null): ?string; + public function fetch(string $key, string $default = null): ?string; /** * Fetch multiple parameters by keys from either GET or POST data. * - * @param array $keys The keys of the parameters to fetch. - * @return array An associative array of parameters. + * @param array $keys The keys of the parameters to fetch. + * @return array An associative array of parameters. */ public function fetchMultiple(array $keys): array; @@ -25,10 +25,10 @@ public function fetchMultiple(array $keys): array; * Fetch a header by key from the request headers. * * @param string $key The key of the header to fetch. - * @param mixed $default Default value if the key is not found. - * @return mixed The value of the header or default. + * @param string|null $default Default value if the key is not found. + * @return string|null The value of the header or default. */ - public function fetchHeader(string $key, $default = null): ?string; + public function fetchHeader(string $key, string $default = null): ?string; /** * Fetch a cookie value by name. diff --git a/src/HelloResponse/HelloResponse.php b/src/HelloResponse/HelloResponse.php index 8f3360f..ee15b36 100644 --- a/src/HelloResponse/HelloResponse.php +++ b/src/HelloResponse/HelloResponse.php @@ -27,7 +27,8 @@ public function setHeader(string $name, $value): void /** * Deletes a cookie by setting its expiration time to the past. * - * This method sets the cookie's value to an empty string and its expiration time to one hour before the current time, which effectively deletes it. + * This method sets the cookie's value to an empty string and its expiration time to one hour before the current + * time, which effectively deletes it. * * @param string $name The name of the cookie to delete. * @param string $path The path on the server where the cookie will be available. Defaults to '/'. @@ -42,15 +43,18 @@ public function deleteCookie(string $name, string $path = '/', string $domain = /** * Sets a cookie with the specified parameters. * - * This method uses PHP's `setcookie` function to set a cookie with the provided name, value, and various optional parameters such as expiration time, path, domain, security, and HttpOnly flags. + * This method uses PHP's `setcookie` function to set a cookie with the provided name, value, and various optional + * parameters such as expiration time, path, domain, security, and HttpOnly flags. * * @param string $name The name of the cookie. * @param string $value The value to store in the cookie. * @param int $expire The expiration time of the cookie, as a Unix timestamp. Defaults to 0 (session cookie). * @param string $path The path for which the cookie is valid. Defaults to '/'. * @param string $domain The domain for which the cookie is valid. Defaults to an empty string. - * @param bool $secure Whether the cookie should only be transmitted over secure (HTTPS) connections. Defaults to false. - * @param bool $httponly Whether the cookie should be accessible only via the HTTP protocol and not JavaScript. Defaults to true. + * @param bool $secure Whether the cookie should only be transmitted over secure (HTTPS) connections. + * Defaults to false. + * @param bool $httponly Whether the cookie should be accessible only via the HTTP protocol and not JavaScript. + * Defaults to true. * @return void */ public function setCookie( @@ -76,7 +80,8 @@ public function setCookie( * Redirects the user to the specified URL. * * This method sends an HTTP Location header to redirect the user to the provided URL and terminates the script. - * If the script is running in a testing environment (i.e., when TESTING is defined), a RuntimeException will be thrown instead of performing the redirect. + * If the script is running in a testing environment (i.e., when TESTING is defined), a RuntimeException will be + * thrown instead of performing the redirect. * * @param string $url The URL to redirect the user to. * @return void @@ -109,11 +114,11 @@ public function render(string $content): string * This method encodes the provided data into a JSON format string, which * can be sent as a response in environments that expect JSON output. * - * @param array $data The data to be converted to a JSON string. + * @param array $data The data to be converted to a JSON string. * @return string The JSON-encoded string representation of the data. */ public function json(array $data): string { - return json_encode($data); + return !json_encode($data) ? "" : json_encode($data); } } diff --git a/src/HelloResponse/HelloResponseInterface.php b/src/HelloResponse/HelloResponseInterface.php index 9c09b9c..96a2473 100644 --- a/src/HelloResponse/HelloResponseInterface.php +++ b/src/HelloResponse/HelloResponseInterface.php @@ -24,8 +24,10 @@ public function setHeader(string $name, $value): void; * @param int $expire The expiration time of the cookie as a Unix timestamp. Default is 0 (session cookie). * @param string $path The path on the server where the cookie will be available. Default is '/'. * @param string $domain The domain that the cookie is available to. Default is an empty string. - * @param bool $secure Indicates if the cookie should only be transmitted over a secure HTTPS connection. Default is false. - * @param bool $httponly When true, makes the cookie accessible only through the HTTP protocol, restricting access from JavaScript. Default is true. + * @param bool $secure Indicates if the cookie should only be transmitted over a secure HTTPS connection. + * Default is false. + * @param bool $httponly When true, makes the cookie accessible only through the HTTP protocol, restricting access + * from JavaScript. Default is true. * @return void */ public function setCookie( @@ -65,8 +67,8 @@ public function redirect(string $url); * different frameworks or environments to implement their own mechanisms * for encoding and returning JSON data. * - * @param array $data The data to be converted to a JSON response. - * @return array The structured JSON response data. + * @param array $data The data to be converted to a JSON response. + * @return string The string encoded JSON response data. */ public function json(array $data): string; diff --git a/src/Lib/AuthHelper.php b/src/Lib/AuthHelper.php index 5cbfba8..43f3a5c 100644 --- a/src/Lib/AuthHelper.php +++ b/src/Lib/AuthHelper.php @@ -14,6 +14,11 @@ public function __construct(PKCE $pkce) { $this->pkce = $pkce; } + + /** + * @param array $config + * @return array + */ public function createAuthRequest(array $config): array { // Validate required parameters @@ -28,20 +33,22 @@ public function createAuthRequest(array $config): array throw new InvalidArgumentException('redirect_uri is required in the authorization request.'); } - $scopes = $config['scope'] ?? []; - if ($scopes && !$this->areScopesValid($scopes)) { - throw new InvalidArgumentException('One or more passed scopes are invalid.'); + $scopes = $config['scope'] ?? Constants::$DEFAULT_SCOPE; + if (is_array($scopes)) { + if (!$this->areScopesValid($scopes)) { + throw new InvalidArgumentException('One or more passed scopes are invalid.'); + } + + // Add 'openid' and ensure uniqueness + $scopes = implode(' ', array_unique(array_merge($scopes, ['openid']))); } - // Add 'openid' and ensure uniqueness - $scopes = array_unique(array_merge($scopes ?? Constants::$DEFAULT_SCOPE, ['openid'])); $nonce = $config['nonce'] ?? $this->generateUuid(); - // Prepare parameters $params = [ 'client_id' => $clientId, 'redirect_uri' => $redirectUri, - 'scope' => implode(' ', $scopes), + 'scope' => $scopes, 'response_type' => $config['response_type'] ?? Constants::$DEFAULT_RESPONSE_TYPE, 'response_mode' => $config['response_mode'] ?? Constants::$DEFAULT_RESPONSE_MODE, 'nonce' => $nonce, @@ -69,7 +76,7 @@ public function createAuthRequest(array $config): array $params['domain_hint'] = $domainHint; } - $wallet = $config['wallet'] ?? Constants::$PRODUCTION_WALLET; + $wallet = !isset($config['wallet']) ? Constants::$PRODUCTION_WALLET : $config['wallet']; $url = $wallet . Constants::$DEFAULT_PATH . '?' . http_build_query($params); return [ @@ -84,6 +91,10 @@ private function isValidScope(string $scope): bool return in_array($scope, Constants::getValidScopes(), true); } + /** + * @param array $scopes + * @return bool + */ private function areScopesValid(array $scopes): bool { foreach ($scopes as $scope) { diff --git a/src/Lib/Crypto.php b/src/Lib/Crypto.php index dbef6b9..66da349 100644 --- a/src/Lib/Crypto.php +++ b/src/Lib/Crypto.php @@ -11,14 +11,22 @@ class Crypto { private string $secret; + /** + * @throws InvalidSecretException + */ public function __construct(string $secret) { if (!$this->checkSecret($secret)) { throw new InvalidSecretException(); } - $this->secret = hex2bin($secret); + $bin = hex2bin($secret); + $this->secret = !$bin ? "" : $bin; } + /** + * @param array $data + * @throws CryptoFailedException + */ public function encrypt(array $data): string { $jsonData = json_encode($data); @@ -38,6 +46,10 @@ public function encrypt(array $data): string return $this->uint8ArrayToUrlSafeBase64($encryptedData); } + /** + * @return array|null + * @throws DecryptionFailedException + */ public function decrypt(string $encryptedStr): ?array { try { @@ -53,13 +65,15 @@ public function decrypt(string $encryptedStr): ?array throw new DecryptionFailedException(); } - return json_decode($decryptedData, true); + /** @var array|null $jsonData */ + $jsonData = json_decode($decryptedData, true); + return $jsonData; } catch (Exception $e) { throw new DecryptionFailedException(); } } - public function checkSecret($secret): bool + public function checkSecret(string $secret): bool { if (!ctype_xdigit($secret) || strlen($secret) % 2 != 0) { return false; @@ -76,7 +90,6 @@ private function uint8ArrayToUrlSafeBase64(string $binaryData): string private function urlSafeBase64ToUint8Array(string $base64String): string { $base64 = strtr($base64String, '-_', '+/'); - $binaryData = base64_decode($base64 . str_repeat('=', (4 - strlen($base64) % 4) % 4)); - return $binaryData; + return base64_decode($base64 . str_repeat('=', (4 - strlen($base64) % 4) % 4)); } } diff --git a/src/Lib/OIDCManager.php b/src/Lib/OIDCManager.php index 66ecca9..433bc14 100644 --- a/src/Lib/OIDCManager.php +++ b/src/Lib/OIDCManager.php @@ -3,6 +3,8 @@ namespace HelloCoop\Lib; use HelloCoop\Config\ConfigInterface; +use HelloCoop\Exception\CryptoFailedException; +use HelloCoop\Exception\DecryptionFailedException; use HelloCoop\Type\OIDC; use HelloCoop\HelloRequest\HelloRequestInterface; use HelloCoop\HelloResponse\HelloResponseInterface; @@ -27,6 +29,9 @@ public function __construct( $this->crypto = $crypto; } + /** + * @throws DecryptionFailedException + */ public function getOidc(): ?OIDC { $oidcCookie = $this->helloRequest->getCookie($this->config->getCookies()['oidcName']); @@ -50,6 +55,9 @@ public function getOidc(): ?OIDC return null; } + /** + * @throws CryptoFailedException + */ public function saveOidc(OIDC $oidc): void { try { diff --git a/src/Lib/PKCE.php b/src/Lib/PKCE.php index f73ce8c..f623b4b 100644 --- a/src/Lib/PKCE.php +++ b/src/Lib/PKCE.php @@ -14,7 +14,7 @@ public function generateVerifier(int $size = self::VERIFIER_LENGTH): string { $mask = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~"; $result = ""; - $randomBytes = random_bytes($size); + $randomBytes = random_bytes(max(1, $size)); // Loop through each byte to generate a random character from the mask for ($i = 0; $i < $size; $i++) { @@ -37,14 +37,12 @@ public function generateChallenge(string $code_verifier): string // Base64 URL encode the hash $encoded = base64_encode($hash); $encoded = rtrim($encoded, '='); - $encoded = str_replace(['/', '+'], ['_', '-'], $encoded); - - return $encoded; + return str_replace(['/', '+'], ['_', '-'], $encoded); } /** Generate a PKCE challenge pair * @param int $length Length of the verifier (between 43-128). Defaults to 43. - * @return array A PKCE challenge pair containing 'code_verifier' and 'code_challenge' + * @return array A PKCE challenge pair containing 'code_verifier' and 'code_challenge' */ public function generatePkce(int $length = self::VERIFIER_LENGTH): array { @@ -68,7 +66,9 @@ public function verifyChallenge(string $code_verifier, string $expectedChallenge return $actualChallenge === $expectedChallenge; } - // Generate a PKCE challenge pair + /** Generate a PKCE challenge pair + * @return array + */ public function generate(): array { $codeVerifier = self::generateVerifier(); diff --git a/src/Lib/TokenFetcher.php b/src/Lib/TokenFetcher.php index 2cf48c3..601dc16 100644 --- a/src/Lib/TokenFetcher.php +++ b/src/Lib/TokenFetcher.php @@ -2,6 +2,7 @@ namespace HelloCoop\Lib; +use Exception; use HelloCoop\Config\Constants; use HelloCoop\Utils\CurlWrapper; @@ -15,6 +16,11 @@ public function __construct(CurlWrapper $curl) $this->curl = $curl; } + /** + * @param array $config + * @return string + * @throws Exception + */ public function fetchToken(array $config): string { $code = $config['code']; @@ -50,6 +56,7 @@ public function fetchToken(array $config): string $this->curl->close($ch); + /** @var array $json */ $json = json_decode($response, true); if ($httpCode !== 200) { @@ -57,11 +64,11 @@ public function fetchToken(array $config): string throw new \Exception($message); } - if (isset($json['error'])) { + if (isset($json['error']) && is_string($json['error'])) { throw new \Exception($json['error']); } - if (!isset($json['id_token'])) { + if (!isset($json['id_token']) || !is_string($json['id_token'])) { throw new \Exception('No id_token in response.'); } diff --git a/src/Lib/TokenParser.php b/src/Lib/TokenParser.php index 99c8140..06f1311 100644 --- a/src/Lib/TokenParser.php +++ b/src/Lib/TokenParser.php @@ -4,6 +4,10 @@ class TokenParser { + /** + * @param string $token + * @return array + */ public function parseToken(string $token): array { $parts = explode('.', $token); @@ -35,7 +39,7 @@ public function parseToken(string $token): array private function base64UrlDecode(string $data): string { - $decodedData = strtr($data, '-_', '+/'); - return base64_decode($decodedData, true); + $decodedData = base64_decode(strtr($data, '-_', '+/'), true); + return !$decodedData ? "" : $decodedData ; } } diff --git a/src/Renderers/DefaultPageRenderer.php b/src/Renderers/DefaultPageRenderer.php index 1834cf3..64d4b2a 100644 --- a/src/Renderers/DefaultPageRenderer.php +++ b/src/Renderers/DefaultPageRenderer.php @@ -14,8 +14,9 @@ public function renderErrorPage( string $targetURI = '/' ): string { return << + + Hellō + + HTML; @@ -128,11 +210,13 @@ public function renderSameSitePage(): string */ public function renderRedirectURIBounce(): string { - $info = $data['info'] ?? 'No additional information provided.'; + // XXX: Is this needed? + // $info = $data['info'] ?? 'No additional information provided.'; return << + +