Skip to content

Commit

Permalink
Merge pull request #68 from aaemnnosttv/fix-66
Browse files Browse the repository at this point in the history
Harden authentication
  • Loading branch information
aaemnnosttv authored Jun 25, 2023
2 parents df57a8a + 2d9efe6 commit ab20aee
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 21 deletions.
40 changes: 30 additions & 10 deletions plugin/wp-cli-login-server.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
);
Expand Down Expand Up @@ -135,6 +136,7 @@ public function run()
$this->loginUser($user);
$this->loginRedirect($user, $magic->redirect_url);
} catch (Exception $e) {
$this->deleteMagic();
$this->abort($e);
}
}
Expand All @@ -155,7 +157,12 @@ 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)) {
// 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, sodium_bin2base64($private_bin, SODIUM_BASE64_VARIANT_URLSAFE))
) {
throw new AuthenticationFailure('Magic login authentication failed.');
}

Expand Down Expand Up @@ -183,14 +190,22 @@ 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.
*
* @param WP_User $user
*/
private function loginUser(WP_User $user)
{
delete_transient($this->magicKey());
$this->deleteMagic();

wp_set_auth_cookie($user->ID);

Expand Down Expand Up @@ -287,25 +302,29 @@ private function abort(Exception $e)
*/
private function magicKey()
{
return self::OPTION . '/' . $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);
}

/**
* 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,
]);
}

Expand All @@ -326,6 +345,7 @@ private function homeUrl()
/**
* @property-read int $user
* @property-read string $private
* @property-read int $expires_at
* @property-read string $redirect_url
*/
class Magic {
Expand Down
14 changes: 8 additions & 6 deletions src/LoginCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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);

Expand All @@ -434,15 +434,17 @@ 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 . '/' . $magic->getKey(),
self::OPTION . '/' . sodium_bin2base64($bin_hash, SODIUM_BASE64_VARIANT_URLSAFE),
json_encode($magic->generate($endpoint)),
ceil($expires)
)) {
WP_CLI::error('Failed to persist magic login.');
}

return;
}

/**
Expand Down
22 changes: 17 additions & 5 deletions src/MagicUrl.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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;
}

Expand All @@ -62,11 +69,15 @@ 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_password($this->signature($endpoint)),
'private' => sodium_bin2base64($private_bin, SODIUM_BASE64_VARIANT_URLSAFE),
'redirect_url' => $this->redirect_url,
'time' => time(),
'expires_at' => $this->expires_at,
];
}

Expand All @@ -84,6 +95,7 @@ private function signature($endpoint)
$endpoint,
$this->domain,
$this->user->ID,
$this->expires_at,
$this->redirect_url,
]);
}
Expand Down

0 comments on commit ab20aee

Please sign in to comment.