Upgrade to 3.2.3
This commit is contained in:
@@ -7,6 +7,7 @@ use Kirby\Exception\PermissionException;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Exception\NotFoundException;
|
||||
use Kirby\Http\Request\Auth\BasicAuth;
|
||||
use Kirby\Toolkit\F;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
@@ -22,10 +23,12 @@ class Auth
|
||||
{
|
||||
protected $impersonate;
|
||||
protected $kirby;
|
||||
protected $user;
|
||||
protected $user = false;
|
||||
protected $userException;
|
||||
|
||||
/**
|
||||
* @param Kirby\Cms\App $kirby
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function __construct(App $kirby)
|
||||
{
|
||||
@@ -79,13 +82,7 @@ class Auth
|
||||
throw new PermissionException('Basic authentication is only allowed over HTTPS');
|
||||
}
|
||||
|
||||
if ($user = $this->kirby->users()->find($auth->username())) {
|
||||
if ($user->validatePassword($auth->password()) === true) {
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return $this->validatePassword($auth->username(), $auth->password());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,24 +155,33 @@ class Auth
|
||||
*/
|
||||
public function ipHash(): string
|
||||
{
|
||||
return hash('sha256', $this->kirby->visitor()->ip());
|
||||
$hash = hash('sha256', $this->kirby->visitor()->ip());
|
||||
|
||||
// only use the first 50 chars to ensure privacy
|
||||
return substr($hash, 0, 50);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if logins are blocked for the current ip
|
||||
* Check if logins are blocked for the current ip or email
|
||||
*
|
||||
* @param string $email
|
||||
* @return boolean
|
||||
*/
|
||||
public function isBlocked(): bool
|
||||
public function isBlocked(string $email): bool
|
||||
{
|
||||
$ip = $this->ipHash();
|
||||
$log = $this->log();
|
||||
$trials = $this->kirby->option('auth.trials', 10);
|
||||
$timeout = $this->kirby->option('auth.timeout', 3600);
|
||||
$ip = $this->ipHash();
|
||||
$log = $this->log();
|
||||
$trials = $this->kirby->option('auth.trials', 10);
|
||||
|
||||
if ($entry = ($log[$ip] ?? null)) {
|
||||
if ($entry['trials'] > $trials) {
|
||||
if ($entry['time'] > (time() - $timeout)) {
|
||||
if ($entry = ($log['by-ip'][$ip] ?? null)) {
|
||||
if ($entry['trials'] >= $trials) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->kirby->users()->find($email)) {
|
||||
if ($entry = ($log['by-email'][$email] ?? null)) {
|
||||
if ($entry['trials'] >= $trials) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -190,18 +196,14 @@ class Auth
|
||||
* @param string $email
|
||||
* @param string $password
|
||||
* @param boolean $long
|
||||
* @return Kirby\Cms\User|false
|
||||
* @return Kirby\Cms\User
|
||||
*
|
||||
* @throws PermissionException If the rate limit was exceeded or if any other error occured with debug mode off
|
||||
* @throws NotFoundException If the email was invalid
|
||||
* @throws InvalidArgumentException If the password is not valid (via `$user->login()`)
|
||||
*/
|
||||
public function login(string $email, string $password, bool $long = false)
|
||||
{
|
||||
// check for blocked ips
|
||||
if ($this->isBlocked() === true) {
|
||||
throw new PermissionException('Rate limit exceeded', 403);
|
||||
}
|
||||
|
||||
// stop impersonating
|
||||
$this->impersonate = null;
|
||||
|
||||
// session options
|
||||
$options = [
|
||||
'createMode' => 'cookie',
|
||||
@@ -209,20 +211,71 @@ class Auth
|
||||
];
|
||||
|
||||
// validate the user and log in to the session
|
||||
if ($user = $this->kirby->users()->find($email)) {
|
||||
if ($user->login($password, $options) === true) {
|
||||
return $this->user = $user;
|
||||
$user = $this->validatePassword($email, $password);
|
||||
$user->loginPasswordless($options);
|
||||
|
||||
// stop impersonating
|
||||
$this->impersonate = null;
|
||||
|
||||
return $this->user = $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the user credentials and returns the user object on success;
|
||||
* otherwise logs the failed attempt
|
||||
*
|
||||
* @param string $email
|
||||
* @param string $password
|
||||
* @return Kirby\Cms\User
|
||||
*
|
||||
* @throws PermissionException If the rate limit was exceeded or if any other error occured with debug mode off
|
||||
* @throws NotFoundException If the email was invalid
|
||||
* @throws InvalidArgumentException If the password is not valid (via `$user->login()`)
|
||||
*/
|
||||
public function validatePassword(string $email, string $password)
|
||||
{
|
||||
// check for blocked ips
|
||||
if ($this->isBlocked($email) === true) {
|
||||
if ($this->kirby->option('debug') === true) {
|
||||
$message = 'Rate limit exceeded';
|
||||
} else {
|
||||
// avoid leaking security-relevant information
|
||||
$message = 'Invalid email or password';
|
||||
}
|
||||
|
||||
throw new PermissionException($message, 403);
|
||||
}
|
||||
|
||||
// log invalid login trial
|
||||
$this->track();
|
||||
// validate the user
|
||||
try {
|
||||
if ($user = $this->kirby->users()->find($email)) {
|
||||
if ($user->validatePassword($password) === true) {
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
|
||||
// sleep for a random amount of milliseconds
|
||||
// to make automated attacks harder
|
||||
usleep(random_int(1000, 2000000));
|
||||
throw new NotFoundException([
|
||||
'key' => 'user.notFound',
|
||||
'data' => [
|
||||
'name' => $email
|
||||
]
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
// log invalid login trial
|
||||
$this->track($email);
|
||||
|
||||
return false;
|
||||
// sleep for a random amount of milliseconds
|
||||
// to make automated attacks harder
|
||||
usleep(random_int(1000, 2000000));
|
||||
|
||||
// keep throwing the original error in debug mode,
|
||||
// otherwise hide it to avoid leaking security-relevant information
|
||||
if ($this->kirby->option('debug') === true) {
|
||||
throw $e;
|
||||
} else {
|
||||
throw new PermissionException('Invalid email or password');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -243,10 +296,39 @@ class Auth
|
||||
public function log(): array
|
||||
{
|
||||
try {
|
||||
return Data::read($this->logfile(), 'json');
|
||||
$log = Data::read($this->logfile(), 'json');
|
||||
$read = true;
|
||||
} catch (Throwable $e) {
|
||||
return [];
|
||||
$log = [];
|
||||
$read = false;
|
||||
}
|
||||
|
||||
// ensure that the category arrays are defined
|
||||
$log['by-ip'] = $log['by-ip'] ?? [];
|
||||
$log['by-email'] = $log['by-email'] ?? [];
|
||||
|
||||
// remove entries that are no longer needed
|
||||
$originalLog = $log;
|
||||
$time = time() - $this->kirby->option('auth.timeout', 3600);
|
||||
foreach ($log as $category => $entries) {
|
||||
$log[$category] = array_filter($entries, function ($entry) use ($time) {
|
||||
return $entry['time'] > $time;
|
||||
});
|
||||
}
|
||||
|
||||
// remove all elements on the top level with different keys (old structure)
|
||||
$log = array_intersect_key($log, array_flip(['by-ip', 'by-email']));
|
||||
|
||||
// write new log to the file system if it changed
|
||||
if ($read === false || $log !== $originalLog) {
|
||||
if (count($log['by-ip']) === 0 && count($log['by-email']) === 0) {
|
||||
F::remove($this->logfile());
|
||||
} else {
|
||||
Data::write($this->logfile(), $log, 'json');
|
||||
}
|
||||
}
|
||||
|
||||
return $log;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -271,26 +353,41 @@ class Auth
|
||||
/**
|
||||
* Tracks a login
|
||||
*
|
||||
* @param string $email
|
||||
* @return boolean
|
||||
*/
|
||||
public function track(): bool
|
||||
public function track(string $email): bool
|
||||
{
|
||||
$ip = $this->ipHash();
|
||||
$log = $this->log();
|
||||
$time = time();
|
||||
|
||||
if (isset($log[$ip]) === true) {
|
||||
$log[$ip] = [
|
||||
if (isset($log['by-ip'][$ip]) === true) {
|
||||
$log['by-ip'][$ip] = [
|
||||
'time' => $time,
|
||||
'trials' => ($log[$ip]['trials'] ?? 0) + 1
|
||||
'trials' => ($log['by-ip'][$ip]['trials'] ?? 0) + 1
|
||||
];
|
||||
} else {
|
||||
$log[$ip] = [
|
||||
$log['by-ip'][$ip] = [
|
||||
'time' => $time,
|
||||
'trials' => 1
|
||||
];
|
||||
}
|
||||
|
||||
if ($this->kirby->users()->find($email)) {
|
||||
if (isset($log['by-email'][$email]) === true) {
|
||||
$log['by-email'][$email] = [
|
||||
'time' => $time,
|
||||
'trials' => ($log['by-email'][$email]['trials'] ?? 0) + 1
|
||||
];
|
||||
} else {
|
||||
$log['by-email'][$email] = [
|
||||
'time' => $time,
|
||||
'trials' => 1
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return Data::write($this->logfile(), $log, 'json');
|
||||
}
|
||||
|
||||
@@ -316,8 +413,9 @@ class Auth
|
||||
/**
|
||||
* Validates the currently logged in user
|
||||
*
|
||||
* @param Kirby\Session\Sessionarray||null $session
|
||||
* @return Kirby\Cms\User|null
|
||||
* @param Kirby\Session\Session|array|null $session
|
||||
* @return Kirby\Cms\User
|
||||
* @throws
|
||||
*/
|
||||
public function user($session = null)
|
||||
{
|
||||
@@ -325,6 +423,18 @@ class Auth
|
||||
return $this->impersonate;
|
||||
}
|
||||
|
||||
// return from cache
|
||||
if ($this->user === null) {
|
||||
// throw the same Exception again if one was captured before
|
||||
if ($this->userException !== null) {
|
||||
throw $this->userException;
|
||||
}
|
||||
|
||||
return null;
|
||||
} elseif ($this->user !== false) {
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
try {
|
||||
if ($this->type() === 'basic') {
|
||||
return $this->user = $this->currentUserFromBasicAuth();
|
||||
@@ -332,7 +442,12 @@ class Auth
|
||||
return $this->user = $this->currentUserFromSession($session);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
return $this->user = null;
|
||||
$this->user = null;
|
||||
|
||||
// capture the Exception for future calls
|
||||
$this->userException = $e;
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user