Upgrade to 3.2.3

This commit is contained in:
Bastian Allgeier
2019-08-06 10:24:12 +02:00
parent 40f095f5de
commit 02be32bb75
48 changed files with 356 additions and 114 deletions

View File

@@ -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;
}
}
}