From e8c860b1cda42887f2ac7d48921145082a2acc9c Mon Sep 17 00:00:00 2001 From: Thorsten Rinne Date: Sat, 14 Sep 2024 11:33:33 +0200 Subject: [PATCH] refactor: use URL from configuration --- phpmyfaq/src/phpMyFAQ/Auth/AuthWebAuthn.php | 85 +++++++++++++++------ phpmyfaq/src/phpMyFAQ/Utils.php | 17 +++++ tests/phpMyFAQ/UtilsTest.php | 38 +++++++++ 3 files changed, 115 insertions(+), 25 deletions(-) diff --git a/phpmyfaq/src/phpMyFAQ/Auth/AuthWebAuthn.php b/phpmyfaq/src/phpMyFAQ/Auth/AuthWebAuthn.php index e446785c51..c34d6ff109 100644 --- a/phpmyfaq/src/phpMyFAQ/Auth/AuthWebAuthn.php +++ b/phpmyfaq/src/phpMyFAQ/Auth/AuthWebAuthn.php @@ -22,11 +22,11 @@ use phpMyFAQ\Auth\WebAuthn\WebAuthnUser; use phpMyFAQ\Configuration; use phpMyFAQ\Core\Exception; +use phpMyFAQ\Utils; use phpseclib3\Crypt\PublicKeyLoader; use phpseclib3\Math\BigInteger; use Random\RandomException; use stdClass; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Session\Session; class AuthWebAuthn extends Auth @@ -43,7 +43,7 @@ public function __construct(Configuration $configuration) { parent::__construct($configuration); - $this->appId = Request::createFromGlobals()->getHost(); + $this->appId = Utils::getHostFromUrl($configuration->getDefaultUrl()); } /** @@ -51,59 +51,94 @@ public function __construct(Configuration $configuration) * * @param string $username * @param string $userId - * @return array + * @return array * @throws RandomException */ public function prepareChallengeForRegistration(string $username, string $userId): array { - $result = new stdClass(); $challenge = random_bytes(16); - $result->challenge = self::stringToArray($challenge); - $result->user = new stdClass(); - $result->user->name = $result->user->displayName = $username; - $result->user->id = self::stringToArray($userId); - $result->rp = new stdClass(); - $result->rp->name = $this->appId; + // Convert the challenge to an array of bytes + $challengeArray = self::stringToArray($challenge); + + // Prepare user information + $user = [ + 'name' => $username, + 'displayName' => $username, + 'id' => self::stringToArray($userId), + ]; - // We need to set the ID only if we are not on localhost + // Prepare relying party (rp) information + $rp = [ + 'name' => $this->appId, + ]; + + // Set the 'id' field if not running on localhost if (!str_contains($this->appId, 'localhost')) { - $result->rp->id = $this->appId; + $rp['id'] = $this->appId; } - $result->pubKeyCredParams = [ + // Prepare public key credential parameters + $pubKeyCredParams = [ [ - 'alg' => self::ES256, + 'alg' => self::ES256, 'type' => 'public-key', ], [ - 'alg' => self::RS256, + 'alg' => self::RS256, 'type' => 'public-key', ], ]; - $result->authenticatorSelection = new stdClass(); - $result->authenticatorSelection->requireResidentKey = false; - $result->authenticatorSelection->userVerification = 'discouraged'; + // Prepare authenticator selection criteria + $authenticatorSelection = [ + 'requireResidentKey' => false, + 'userVerification' => 'discouraged', + ]; + + // Prepare extensions + $extensions = [ + 'exts' => true, + ]; + + // Build the publicKey object + $publicKey = [ + 'challenge' => $challengeArray, + 'user' => $user, + 'rp' => $rp, + 'pubKeyCredParams' => $pubKeyCredParams, + 'authenticatorSelection' => $authenticatorSelection, + 'attestation' => null, + 'timeout' => 60000, + 'excludeCredentials' => [], + 'extensions' => $extensions, + ]; - $result->attestation = null; - $result->timeout = 60000; - $result->excludeCredentials = []; // No excludeList - $result->extensions = new stdClass(); - $result->extensions->exts = true; + // Base64 URL-encode the challenge for later verification + $b64challenge = rtrim(strtr(base64_encode($challenge), '+/', '-_'), '='); + // Return the prepared data return [ - 'publicKey' => $result, - 'b64challenge' => rtrim(strtr(base64_encode($challenge), '+/', '-_'), '='), + 'publicKey' => $publicKey, + 'b64challenge' => $b64challenge, ]; } + /** + * Store the WebAuth user information in the session + * @param WebAuthnUser $user + * @return void + */ public function storeUserInSession(WebAuthnUser $user): void { $session = new Session(); $session->set('webauthn', $user); } + /** + * Get the WebAuth user information from the session + * @return WebAuthnUser|null + */ public function getUserFromSession(): ?WebAuthnUser { $session = new Session(); diff --git a/phpmyfaq/src/phpMyFAQ/Utils.php b/phpmyfaq/src/phpMyFAQ/Utils.php index 287a4b4d35..9f8d73a89e 100644 --- a/phpmyfaq/src/phpMyFAQ/Utils.php +++ b/phpmyfaq/src/phpMyFAQ/Utils.php @@ -238,6 +238,23 @@ public static function parseUrl(string $string): string return preg_replace($pattern, $replacement, $string); } + /** + * Extracts the hostname from a given URL. + * + * @param string $url The URL from which to extract the hostname. + * @return string|null The hostname or null if the URL is invalid. + */ + public static function getHostFromUrl(string $url): ?string + { + $parsedUrl = parse_url($url); + + if ($parsedUrl && isset($parsedUrl['host'])) { + return $parsedUrl['host']; + } + + return null; + } + /** * Moves given key of an array to the top * diff --git a/tests/phpMyFAQ/UtilsTest.php b/tests/phpMyFAQ/UtilsTest.php index 5a54aa9a1d..bf303d5a39 100644 --- a/tests/phpMyFAQ/UtilsTest.php +++ b/tests/phpMyFAQ/UtilsTest.php @@ -135,6 +135,44 @@ public function testParseUrl(): void } } + public function testGetHostFromValidUrl(): void + { + $url = 'https://example.com/path?query=param'; + $host = Utils::getHostFromUrl($url); + $this->assertEquals('example.com', $host); + } + + public function testGetHostFromUrlWithSubdomain(): void + { + $url = 'https://sub.example.com/path'; + $host = Utils::getHostFromUrl($url); + $this->assertEquals('sub.example.com', $host); + } + + public function testGetHostFromUrlWithoutScheme(): void + { + $url = 'example.com/path'; + $host = Utils::getHostFromUrl($url); + $this->assertNull($host); // parse_url won't recognize 'example.com' without scheme + } + + public function testGetHostFromInvalidUrl(): void + { + $url = 'not_a_url'; + $host = Utils::getHostFromUrl($url); + $this->assertNull($host); + } + + /** + * Test URL without a host part. + */ + public function testGetHostFromUrlWithoutHost(): void + { + $url = 'ftp://user:password@'; + $host = Utils::getHostFromUrl($url); + $this->assertNull($host); + } + public function testChopString(): void { // Test case 1: String has more words than desired length