Skip to content

Commit

Permalink
feat: Store valid and failed login attempts
Browse files Browse the repository at this point in the history
Relates #814
  • Loading branch information
geokrety-bot committed Jul 31, 2023
1 parent 4801e81 commit 03e6b10
Show file tree
Hide file tree
Showing 6 changed files with 402 additions and 8 deletions.
19 changes: 17 additions & 2 deletions website/app/GeoKrety/Controller/Pages/Login.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

class Login extends Base {
private const LEGACY_API2SECID_ERROR_STRING = 'error %d ';
private const PASSWORD_CREDENTIALS_FAILS_ERROR = 1;
private const PASSWORD_INVALID_ACCOUNT_ERROR = 2;
private const API2SECID_CREDENTIALS_FAILS_ERROR = 1;
private const API2SECID_INVALID_ACCOUNT_ERROR = 2;
private const API2SECID_EMPTY_CREDENTIALS_ERROR = 3;
Expand All @@ -37,8 +39,14 @@ public function login(\Base $f3) {
$auth = new Auth('password', ['id' => 'username', 'pw' => 'password']);
$user = $auth->login($f3->get('POST.login'), $f3->get('POST.password'));
if ($user !== false) {
Event::instance()->emit('user.login.password', $user);
LanguageService::changeLanguageTo($user->preferred_language);
if ($user->isAccountInvalid() && !$user->isAccountImported()) {
Event::instance()->emit('user.login.password-failure', [
'username' => $f3->get('POST.login'),
'error' => self::PASSWORD_INVALID_ACCOUNT_ERROR,
'error_message' => 'Your account is not valid.',
]);
if (GK_DEVEL) {
$user->resendAccountActivationEmail();
\Base::instance()->reroute('@home');
Expand All @@ -54,6 +62,11 @@ public function login(\Base $f3) {
}
$this::connectUser($f3, $user, 'password');
} else {
Event::instance()->emit('user.login.password-failure', [
'username' => $f3->get('POST.login'),
'error' => self::PASSWORD_CREDENTIALS_FAILS_ERROR,
'error_message' => 'Username and password doesn\'t match.',
]);
Flash::instance()->addMessage(_('Username and password doesn\'t match.'), 'danger');
}
$this->loginForm($f3);
Expand All @@ -78,7 +91,7 @@ public static function connectUser(\Base $f3, User $user, ?string $method = null
$f3->set('SESSION.IS_ADMIN', false);
}
Smarty::assign('current_user', $user);
Event::instance()->emit("user.login.$method", $user);
Event::instance()->emit("user.login.$method-effective", $user);
if (in_array($method, self::NO_GRAPHIC_LOGIN)) {
return;
}
Expand Down Expand Up @@ -162,6 +175,7 @@ public function login2Secid_post(\Base $f3) {
$auth = new Auth('password', ['id' => 'username', 'pw' => 'password']);
$user = $auth->login($f3->get('POST.login'), $f3->get('POST.password'));
if ($user !== false) {
Event::instance()->emit('user.login.api2secid', $user);
if ($user->isAccountInvalid() && !$user->isAccountImported()) {
http_response_code(400); // TODO what is the most logical code ? probably not 400 neither 500
echo $this->getApi2SecidLegacyError(self::API2SECID_INVALID_ACCOUNT_ERROR);
Expand All @@ -176,7 +190,7 @@ public function login2Secid_post(\Base $f3) {
exit();
}
echo $user->secid;
Event::instance()->emit('user.login.api2secid', $user);
Event::instance()->emit('user.login.api2secid.effective', $user);
Login::disconnectUser($f3);
exit();
}
Expand Down Expand Up @@ -217,6 +231,7 @@ public function secidAuth(\Base $f3, ?string $secid, bool $streamXML = true): Us
]);
exit();
}
Event::instance()->emit('user.login.secid', $user);
Login::connectUser($f3, $user, 'secid');

return $user;
Expand Down
141 changes: 141 additions & 0 deletions website/app/GeoKrety/Model/UsersAuthenticationHistory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<?php

namespace GeoKrety\Model;

use DateTime;
use DB\SQL\Schema;

/**
* @property int|null id
* @property int|User user Authentication attempt for user
* @property string username Entered username
* @property string|null user_agent
* @property string ip
* @property string method
* @property string session
* @property bool succeed
* @property string|null comment
* @property DateTime created_on_datetime
* @property DateTime updated_on_datetime
*/
class UsersAuthenticationHistory extends Base {
use \Validation\Traits\CortexTrait;

public const METHOD_PASSWORD = 'password';
public const METHOD_SECID = 'secid';
public const METHOD_DEVEL = 'devel';
public const METHOD_OAUTH = 'oauth';
public const METHOD_REGISTRATION_ACTIVATE = 'registration.activate';
public const METHOD_REGISTRATION_OAUTH = 'registration.oauth';

public const METHOD_API2SECID = 'api2secid';

public const VALID_METHODS = [
self::METHOD_PASSWORD,
self::METHOD_SECID,
self::METHOD_DEVEL,
self::METHOD_OAUTH,
self::METHOD_REGISTRATION_ACTIVATE,
self::METHOD_REGISTRATION_OAUTH,
self::METHOD_API2SECID,
];

protected $db = 'DB';
protected $table = 'gk_users_authentication_history';

protected $fieldConf = [
'user' => [
'belongs-to-one' => '\GeoKrety\Model\User',
'validate' => 'required',
'nullable' => true,
],
'username' => [
'type' => Schema::DT_VARCHAR128,
'nullable' => true,
],
'user_agent' => [
'type' => Schema::DT_TEXT,
'nullable' => true,
],
'ip' => [
'type' => Schema::DT_VARCHAR256,
'nullable' => false,
'validate' => 'not_empty',
],
'method' => [
'type' => Schema::DT_VARCHAR256,
'nullable' => false,
'filter' => 'trim',
'validate' => 'valid_authentication_method',
],
'session' => [
'type' => Schema::DT_VARCHAR256,
'nullable' => false,
],
'succeed' => [
'type' => Schema::DT_BOOLEAN,
'nullable' => false,
],
'comment' => [
'type' => Schema::DT_TEXT,
'nullable' => true,
],
'created_on_datetime' => [
'type' => Schema::DT_DATETIME,
'default' => 'CURRENT_TIMESTAMP',
'nullable' => true,
'validate' => 'is_date',
],
'updated_on_datetime' => [
'type' => Schema::DT_DATETIME,
'nullable' => true,
'validate' => 'is_date',
],
];

public function get_created_on_datetime($value): ?DateTime {
return self::get_date_object($value);
}

public function get_updated_on_datetime($value): ?DateTime {
return self::get_date_object($value);
}

public static function is_valid_method($method) {
return in_array($method, self::VALID_METHODS, true);
}

public function __construct() {
parent::__construct();
$this->beforeinsert(function ($self) {
$self->session = session_id();
$self->ip = \Base::instance()->get('IP');
$self->user_agent = \Base::instance()->get('AGENT');
});
}

/**
* @throws \Exception
*/
public static function save_authentication_history(string $username, $method, ?User $user = null, bool $succeed = true, string $comment = null) {
if (!self::is_valid_method($method)) {
throw new Exception('Invalid Authentication Method');
}
$history = new \GeoKrety\Model\UsersAuthenticationHistory();
$history->username = $username;
$history->method = $method;
$history->user = $user;
$history->succeed = $succeed;
$history->comment = $comment;
$history->save();
}

public function jsonSerialize() {
return [
'user' => $this->getRaw('user'),
'ip' => $this->ip,
'user_agent' => $this->user_agent,
'method' => $this->method,
];
}
}
84 changes: 78 additions & 6 deletions website/app/events.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,33 +64,105 @@ function audit(string $event, $newObjectModel) {
});
$events->on('user.login.password', function (GeoKrety\Model\User $user) {
audit('user.login.password', $user);
Session::setUserId($user);
\GeoKrety\Model\UsersAuthenticationHistory::save_authentication_history(
$user->username,
\GeoKrety\Model\UsersAuthenticationHistory::METHOD_PASSWORD,
$user,
);
Metrics::counter('logged_in_users_total', 'Total number of connections', ['type'], ['password']);
});
$events->on('user.login.password-effective', function (GeoKrety\Model\User $user) {
audit('user.login.password-effective', $user);
Session::setUserId($user);
Metrics::counter('logged_in_users_effective_total', 'Total number of effective connections', ['type'], ['password']);
});
$events->on('user.login.password-failure', function (array $context) {
audit('user.login.password-failure', $context);
\GeoKrety\Model\UsersAuthenticationHistory::save_authentication_history(
$context['username'],
\GeoKrety\Model\UsersAuthenticationHistory::METHOD_PASSWORD,
null,
false,
$context['error_message'],
);
Metrics::counter('logged_in_failure_total', 'Total number of connections failures', ['type'], ['password']);
});
$events->on('user.login.secid', function (GeoKrety\Model\User $user) {
audit('user.login.secid', $user);
Session::setUserId($user);
\GeoKrety\Model\UsersAuthenticationHistory::save_authentication_history(
$user->username,
\GeoKrety\Model\UsersAuthenticationHistory::METHOD_SECID,
$user,
);
Metrics::counter('logged_in_users_total', 'Total number of connections', ['type'], ['secid']);
});
$events->on('user.login.secid-effective', function (GeoKrety\Model\User $user) {
audit('user.login.secid-effective', $user);
Session::setUserId($user);
Metrics::counter('logged_in_users_effective_total', 'Total number of effective connections', ['type'], ['secid']);
});
$events->on('user.login.secid-failure', function (array $context) {
audit('user.login.secid-failure', $context);
\GeoKrety\Model\UsersAuthenticationHistory::save_authentication_history(
$context['username'],
\GeoKrety\Model\UsersAuthenticationHistory::METHOD_SECID,
null,
false,
$context['error_message'],
);
Metrics::counter('logged_in_failure_total', 'Total number of connections failures', ['type'], ['secid']);
});
$events->on('user.login.api2secid', function (GeoKrety\Model\User $user) {
audit('user.login.api2secid', $user);
Session::setUserId($user);
\GeoKrety\Model\UsersAuthenticationHistory::save_authentication_history(
$user->username,
\GeoKrety\Model\UsersAuthenticationHistory::METHOD_API2SECID,
$user,
);
Metrics::counter('logged_in_users_total', 'Total number of connections', ['type'], ['api2secid']);
});
$events->on('user.login.api2secid-effective', function (GeoKrety\Model\User $user) {
audit('user.login.api2secid-effective', $user);
Session::setUserId($user);
Metrics::counter('logged_in_users_effective_total', 'Total number of effective connections', ['type'], ['api2secid']);
save_authentication_history($user, \GeoKrety\Model\UsersAuthenticationHistory::METHOD_API2SECID);
});
$events->on('user.login.api2secid-failure', function (array $context) {
audit('user.login.api2secid-failure', $context);
\GeoKrety\Model\UsersAuthenticationHistory::save_authentication_history(
$context['username'],
\GeoKrety\Model\UsersAuthenticationHistory::METHOD_API2SECID,
null,
false,
$context['error_message'],
);
Metrics::counter('logged_in_failure_total', 'Total number of connections failures', ['type'], ['api2secid']);
});
$events->on('user.login.oauth', function (GeoKrety\Model\User $user) {
audit('user.login.oauth', $user);
$events->on('user.login.oauth-effective', function (GeoKrety\Model\User $user) {
audit('user.login.oauth-effective', $user);
Session::setUserId($user);
\GeoKrety\Model\UsersAuthenticationHistory::save_authentication_history(
$user->username,
\GeoKrety\Model\UsersAuthenticationHistory::METHOD_OAUTH,
$user,
);
Metrics::counter('logged_in_users_total', 'Total number of connections', ['type'], ['oauth']);
Metrics::counter('logged_in_users_effective_total', 'Total number of effective connections', ['type'], ['oauth']);
});
$events->on('user.login.devel', function (GeoKrety\Model\User $user) {
audit('user.login.devel', $user);
Session::setUserId($user);
\GeoKrety\Model\UsersAuthenticationHistory::save_authentication_history(
$user->username,
\GeoKrety\Model\UsersAuthenticationHistory::METHOD_DEVEL,
$user,
);
Metrics::counter('logged_in_users_total', 'Total number of connections', ['type'], ['devel']);
});
$events->on('user.login.devel-effective', function (GeoKrety\Model\User $user) {
audit('user.login.devel-effective', $user);
Session::setUserId($user);
Metrics::counter('logged_in_users_effective_total', 'Total number of effective connections', ['type'], ['devel']);
});
$events->on('user.login.registration.oauth', function (GeoKrety\Model\User $user) {
audit('user.login.registration.oauth', $user);
Metrics::counter('registration_type_total', 'Total number of registration types', ['type'], ['oauth']);
Expand Down
4 changes: 4 additions & 0 deletions website/app/validators.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@
return $input[$field] !== false;
}, 'Invalid value for {0}');

$validator->addValidator('valid_authentication_method', function ($field, $input, $param = null) {
return \GeoKrety\Model\UsersAuthenticationHistory::is_valid_method($input[$field]);
}, 'Invalid value for {0}');

$validator->addFilter('HTMLPurifier', function ($value, $params = null) {
return \GeoKrety\Service\HTMLPurifier::getPurifier()->purify($value);
});
Expand Down
Loading

0 comments on commit 03e6b10

Please sign in to comment.