From 74d60ecfc46029c6c96f4f36307a10e8f4fbbb63 Mon Sep 17 00:00:00 2001 From: Evan Mattson Date: Thu, 11 May 2023 23:10:47 -0400 Subject: [PATCH 01/12] Update use of strtoupper to avoid deprecation notice --- plugin/wp-cli-login-server.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugin/wp-cli-login-server.php b/plugin/wp-cli-login-server.php index 7c49de6..306cf4e 100644 --- a/plugin/wp-cli-login-server.php +++ b/plugin/wp-cli-login-server.php @@ -36,7 +36,8 @@ function is_eligible_request() || (defined('DOING_AJAX') && DOING_AJAX) // ignore ajax requests || (defined('DOING_CRON') && DOING_CRON) // ignore cron requests || (defined('WP_INSTALLING') && WP_INSTALLING) // WP ain't ready - || 'GET' != strtoupper(@$_SERVER['REQUEST_METHOD']) // GET requests only + || empty($_SERVER['REQUEST_METHOD']) // Invalid request + || 'GET' !== strtoupper($_SERVER['REQUEST_METHOD']) // GET requests only || count($_GET) > 0 // if there is any query string || is_admin() // ignore admin requests ); From 5d2cfcfc9634497184f91055ea9f806a9e26e1d4 Mon Sep 17 00:00:00 2001 From: Evan Mattson Date: Thu, 11 May 2023 23:11:33 -0400 Subject: [PATCH 02/12] Hash public key in magic key --- plugin/wp-cli-login-server.php | 2 +- src/LoginCommand.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/wp-cli-login-server.php b/plugin/wp-cli-login-server.php index 306cf4e..aa23971 100644 --- a/plugin/wp-cli-login-server.php +++ b/plugin/wp-cli-login-server.php @@ -288,7 +288,7 @@ private function abort(Exception $e) */ private function magicKey() { - return self::OPTION . '/' . $this->publicKey; + return self::OPTION . '/' . wp_hash($this->publicKey); } /** diff --git a/src/LoginCommand.php b/src/LoginCommand.php index e57a918..baba6d2 100644 --- a/src/LoginCommand.php +++ b/src/LoginCommand.php @@ -435,7 +435,7 @@ private function makeMagicUrl(WP_User $user, $expires, $redirect_url) private function persistMagicUrl(MagicUrl $magic, $endpoint, $expires) { if (! set_transient( - self::OPTION . '/' . $magic->getKey(), + self::OPTION . '/' . wp_hash($magic->getKey()), json_encode($magic->generate($endpoint)), ceil($expires) )) { From 45074140855976f5f9ae8a932878019bdbe2f968 Mon Sep 17 00:00:00 2001 From: Evan Mattson Date: Thu, 11 May 2023 23:12:41 -0400 Subject: [PATCH 03/12] Use wp_hash and hash_equals instead of password functions --- plugin/wp-cli-login-server.php | 4 +++- src/MagicUrl.php | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/plugin/wp-cli-login-server.php b/plugin/wp-cli-login-server.php index aa23971..61237ad 100644 --- a/plugin/wp-cli-login-server.php +++ b/plugin/wp-cli-login-server.php @@ -156,7 +156,9 @@ private function validate(Magic $magic) throw new InvalidUser('No user found or no longer exists.'); } - if (! $magic->private || ! wp_check_password($this->signature($user, $magic->redirect_url), $magic->private)) { + if (! $magic->private + || ! hash_equals(wp_hash($this->signature($user, $magic->redirect_url)), $magic->private) + ) { throw new AuthenticationFailure('Magic login authentication failed.'); } diff --git a/src/MagicUrl.php b/src/MagicUrl.php index 7a33ab2..94bd042 100644 --- a/src/MagicUrl.php +++ b/src/MagicUrl.php @@ -64,7 +64,7 @@ public function generate($endpoint) { return [ 'user' => $this->user->ID, - 'private' => wp_hash_password($this->signature($endpoint)), + 'private' => wp_hash($this->signature($endpoint)), 'redirect_url' => $this->redirect_url, 'time' => time(), ]; From dd32830dd74744bc4e3fe2adddf7f0a3c3a1f4bd Mon Sep 17 00:00:00 2001 From: Evan Mattson Date: Thu, 11 May 2023 23:13:11 -0400 Subject: [PATCH 04/12] Remove redundant return --- src/LoginCommand.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/LoginCommand.php b/src/LoginCommand.php index baba6d2..cdd7423 100644 --- a/src/LoginCommand.php +++ b/src/LoginCommand.php @@ -441,8 +441,6 @@ private function persistMagicUrl(MagicUrl $magic, $endpoint, $expires) )) { WP_CLI::error('Failed to persist magic login.'); } - - return; } /** From c9f43dbb22571b46f06142e4c8160740cfbbef2c Mon Sep 17 00:00:00 2001 From: Evan Mattson Date: Thu, 11 May 2023 23:13:37 -0400 Subject: [PATCH 05/12] Bump server plugin and required version --- plugin/wp-cli-login-server.php | 2 +- src/LoginCommand.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/wp-cli-login-server.php b/plugin/wp-cli-login-server.php index 61237ad..75bb70a 100644 --- a/plugin/wp-cli-login-server.php +++ b/plugin/wp-cli-login-server.php @@ -6,7 +6,7 @@ * Author URI: https://aaemnnost.tv * Plugin URI: https://aaemnnost.tv/wp-cli-commands/login/ * - * Version: 1.4 + * Version: 1.5 */ namespace WP_CLI_Login; diff --git a/src/LoginCommand.php b/src/LoginCommand.php index cdd7423..9556f6d 100644 --- a/src/LoginCommand.php +++ b/src/LoginCommand.php @@ -21,7 +21,7 @@ class LoginCommand /** * Required version constraint of the wp-cli-login-server companion plugin. */ - const REQUIRED_PLUGIN_VERSION = '^1.3'; + const REQUIRED_PLUGIN_VERSION = '^1.5'; /** * Package instance From 86d8288c29f47e15614562e9335038eeada4aa08 Mon Sep 17 00:00:00 2001 From: Evan Mattson Date: Thu, 11 May 2023 23:17:27 -0400 Subject: [PATCH 06/12] Fix case in confirm prompt to highlight default --- src/LoginCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LoginCommand.php b/src/LoginCommand.php index 9556f6d..8e6512e 100644 --- a/src/LoginCommand.php +++ b/src/LoginCommand.php @@ -281,7 +281,7 @@ private function promptForReset($version = null) */ private function confirm($question) { - fwrite(STDOUT, $question . ' [Y/n] '); + fwrite(STDOUT, $question . ' [y/N] '); $response = trim(fgets(STDIN)); return ('y' == strtolower($response)); From 20c2e674bcbcac7fdc85b8449273940ee0fae8d9 Mon Sep 17 00:00:00 2001 From: Evan Mattson Date: Fri, 12 May 2023 22:48:18 -0400 Subject: [PATCH 07/12] Add expiration to signature --- plugin/wp-cli-login-server.php | 11 ++++++----- src/LoginCommand.php | 2 +- src/MagicUrl.php | 16 ++++++++++++---- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/plugin/wp-cli-login-server.php b/plugin/wp-cli-login-server.php index 75bb70a..41d1b43 100644 --- a/plugin/wp-cli-login-server.php +++ b/plugin/wp-cli-login-server.php @@ -296,19 +296,19 @@ private function magicKey() /** * Build the signature to check against the private key for this request. * - * @param WP_User $user - * @param string $redirect_url + * @param Magic Login data. * * @return string */ - private function signature(WP_User $user, $redirect_url) + private function signature(Magic $magic) { return join('|', [ $this->publicKey, $this->endpoint, parse_url($this->homeUrl(), PHP_URL_HOST), - $user->ID, - $redirect_url, + $magic->user, + $magic->expires_at, + $magic->redirect_url, ]); } @@ -329,6 +329,7 @@ private function homeUrl() /** * @property-read int $user * @property-read string $private + * @property-read int $expires_at * @property-read string $redirect_url */ class Magic { diff --git a/src/LoginCommand.php b/src/LoginCommand.php index 8e6512e..0c11af9 100644 --- a/src/LoginCommand.php +++ b/src/LoginCommand.php @@ -418,7 +418,7 @@ private function makeMagicUrl(WP_User $user, $expires, $redirect_url) static::debug("Generating a new magic login for User $user->ID expiring in {$expires} seconds."); $endpoint = $this->endpoint(); - $magic = new MagicUrl($user, $this->domain(), $redirect_url); + $magic = new MagicUrl($user, $this->domain(), time() + $expires, $redirect_url); $this->persistMagicUrl($magic, $endpoint, $expires); diff --git a/src/MagicUrl.php b/src/MagicUrl.php index 94bd042..f607578 100644 --- a/src/MagicUrl.php +++ b/src/MagicUrl.php @@ -23,6 +23,11 @@ class MagicUrl */ private $domain; + /** + * @var int Timestamp that the magic url is valid until. + */ + private $expires_at; + /** * @var string URL to redirect to upon successful login. */ @@ -32,14 +37,16 @@ class MagicUrl * MagicUrl constructor. * * @param WP_User $user - * @param string $domain - * @param string $redirect_url + * @param string $domain + * @param int $expires_at + * @param string $redirect_url */ - public function __construct(WP_User $user, $domain, $redirect_url = null) + public function __construct(WP_User $user, $domain, $expires_at, $redirect_url = null) { $this->user = $user; $this->domain = $domain; $this->key = $this->newPublicKey(); + $this->expires_at = ceil($expires_at); $this->redirect_url = $redirect_url; } @@ -66,7 +73,7 @@ public function generate($endpoint) 'user' => $this->user->ID, 'private' => wp_hash($this->signature($endpoint)), 'redirect_url' => $this->redirect_url, - 'time' => time(), + 'expires_at' => $this->expires_at, ]; } @@ -84,6 +91,7 @@ private function signature($endpoint) $endpoint, $this->domain, $this->user->ID, + $this->expires_at, $this->redirect_url, ]); } From b28835c6834fa3679c132724f9739774faa0f4fc Mon Sep 17 00:00:00 2001 From: Evan Mattson Date: Fri, 12 May 2023 22:49:08 -0400 Subject: [PATCH 08/12] Delete transient on hash mismatch --- plugin/wp-cli-login-server.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugin/wp-cli-login-server.php b/plugin/wp-cli-login-server.php index 41d1b43..27b35c2 100644 --- a/plugin/wp-cli-login-server.php +++ b/plugin/wp-cli-login-server.php @@ -135,6 +135,9 @@ public function run() $user = $this->validate($magic); $this->loginUser($user); $this->loginRedirect($user, $magic->redirect_url); + } catch (AuthenticationFailure $e) { + delete_transient($this->magicKey()); + $this->abort($e); } catch (Exception $e) { $this->abort($e); } From 755255fc83704f4c7a72abf10a12bac6a630e3c6 Mon Sep 17 00:00:00 2001 From: Evan Mattson Date: Fri, 12 May 2023 22:52:23 -0400 Subject: [PATCH 09/12] Correct usage of hash_equals --- plugin/wp-cli-login-server.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/wp-cli-login-server.php b/plugin/wp-cli-login-server.php index 27b35c2..7bf370e 100644 --- a/plugin/wp-cli-login-server.php +++ b/plugin/wp-cli-login-server.php @@ -160,7 +160,7 @@ private function validate(Magic $magic) } if (! $magic->private - || ! hash_equals(wp_hash($this->signature($user, $magic->redirect_url)), $magic->private) + || ! hash_equals($magic->private, wp_hash($this->signature($magic))) ) { throw new AuthenticationFailure('Magic login authentication failed.'); } From 26d861eebd63bb34e95150ccf5bed8f993bba1fa Mon Sep 17 00:00:00 2001 From: Evan Mattson Date: Fri, 12 May 2023 23:01:50 -0400 Subject: [PATCH 10/12] Extract magic deletion to method --- plugin/wp-cli-login-server.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/plugin/wp-cli-login-server.php b/plugin/wp-cli-login-server.php index 7bf370e..a6bd7ef 100644 --- a/plugin/wp-cli-login-server.php +++ b/plugin/wp-cli-login-server.php @@ -136,7 +136,7 @@ public function run() $this->loginUser($user); $this->loginRedirect($user, $magic->redirect_url); } catch (AuthenticationFailure $e) { - delete_transient($this->magicKey()); + $this->deleteMagic(); $this->abort($e); } catch (Exception $e) { $this->abort($e); @@ -189,6 +189,14 @@ private function loadMagic() return new Magic($magic); } + /** + * Delete saved magic. + */ + private function deleteMagic() + { + delete_transient($this->magicKey()); + } + /** * Login the given user and redirect them to wp-admin. * @@ -196,7 +204,7 @@ private function loadMagic() */ private function loginUser(WP_User $user) { - delete_transient($this->magicKey()); + $this->deleteMagic(); wp_set_auth_cookie($user->ID); From b4a7d9a6011d6a3668094cef653ddcfefbcd55cf Mon Sep 17 00:00:00 2001 From: Evan Mattson Date: Fri, 2 Jun 2023 23:18:39 -0400 Subject: [PATCH 11/12] Replace wp_hash with sodium --- plugin/wp-cli-login-server.php | 11 +++++++++-- src/LoginCommand.php | 6 +++++- src/MagicUrl.php | 6 +++++- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/plugin/wp-cli-login-server.php b/plugin/wp-cli-login-server.php index a6bd7ef..11fec6c 100644 --- a/plugin/wp-cli-login-server.php +++ b/plugin/wp-cli-login-server.php @@ -159,8 +159,11 @@ private function validate(Magic $magic) throw new InvalidUser('No user found or no longer exists.'); } + // We need to hash the salt to produce a key that won't exceed the maximum of 64 bytes. + $key = sodium_crypto_generichash(wp_salt('auth')); + $private_bin = sodium_crypto_generichash($this->signature($magic), $key); if (! $magic->private - || ! hash_equals($magic->private, wp_hash($this->signature($magic))) + || ! hash_equals($magic->private, sodium_bin2base64($private_bin, SODIUM_BASE64_VARIANT_URLSAFE)) ) { throw new AuthenticationFailure('Magic login authentication failed.'); } @@ -301,7 +304,11 @@ private function abort(Exception $e) */ private function magicKey() { - return self::OPTION . '/' . wp_hash($this->publicKey); + // We need to hash the salt to produce a key that won't exceed the maximum of 64 bytes. + $key = sodium_crypto_generichash(wp_salt('auth')); + $bin_hash = sodium_crypto_generichash($this->publicKey, $key); + + return self::OPTION . '/' . sodium_bin2base64($bin_hash, SODIUM_BASE64_VARIANT_URLSAFE); } /** diff --git a/src/LoginCommand.php b/src/LoginCommand.php index 0c11af9..ab25311 100644 --- a/src/LoginCommand.php +++ b/src/LoginCommand.php @@ -434,8 +434,12 @@ private function makeMagicUrl(WP_User $user, $expires, $redirect_url) */ private function persistMagicUrl(MagicUrl $magic, $endpoint, $expires) { + // We need to hash the salt to produce a key that won't exceed the maximum of 64 bytes. + $key = sodium_crypto_generichash(wp_salt('auth')); + $bin_hash = sodium_crypto_generichash($magic->getKey(), $key); + if (! set_transient( - self::OPTION . '/' . wp_hash($magic->getKey()), + self::OPTION . '/' . sodium_bin2base64($bin_hash, SODIUM_BASE64_VARIANT_URLSAFE), json_encode($magic->generate($endpoint)), ceil($expires) )) { diff --git a/src/MagicUrl.php b/src/MagicUrl.php index f607578..009760f 100644 --- a/src/MagicUrl.php +++ b/src/MagicUrl.php @@ -69,9 +69,13 @@ public function getKey() */ public function generate($endpoint) { + // We need to hash the salt to produce a key that won't exceed the maximum of 64 bytes. + $key = sodium_crypto_generichash(wp_salt('auth')); + $private_bin = sodium_crypto_generichash($this->signature($endpoint), $key); + return [ 'user' => $this->user->ID, - 'private' => wp_hash($this->signature($endpoint)), + 'private' => sodium_bin2base64($private_bin, SODIUM_BASE64_VARIANT_URLSAFE), 'redirect_url' => $this->redirect_url, 'expires_at' => $this->expires_at, ]; From 2d9efe673ba349d3590d6887ba6750327f948d96 Mon Sep 17 00:00:00 2001 From: Evan Mattson Date: Fri, 2 Jun 2023 23:33:29 -0400 Subject: [PATCH 12/12] Delete magic on all exceptions --- plugin/wp-cli-login-server.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugin/wp-cli-login-server.php b/plugin/wp-cli-login-server.php index 11fec6c..32e609b 100644 --- a/plugin/wp-cli-login-server.php +++ b/plugin/wp-cli-login-server.php @@ -135,10 +135,8 @@ public function run() $user = $this->validate($magic); $this->loginUser($user); $this->loginRedirect($user, $magic->redirect_url); - } catch (AuthenticationFailure $e) { - $this->deleteMagic(); - $this->abort($e); } catch (Exception $e) { + $this->deleteMagic(); $this->abort($e); } }