Skip to content
Permalink
Browse files Browse the repository at this point in the history
Add JWT anti brute-force login protection (#2831)
* WIP: better brute force login protection

* split device token classes

* mv schema to 63

* use int(10) in schema too

* add sysadmin action to clear locked users/devices

* remove the FK on authfail

* remove authfail users_id fk constraint in structure.sql

* catch the invalid device token exception

* remove the banned users stuff

* change invalid token message

* cleanup the exceptions a bit

* get rid of the useless InvalidCsrfTokenException

* remove unused js import

* introduce the AuthenticatedUser and AnonymousUser classes

and improve the App and init.inc.php files

* remove the populateFromEmail method from Users

* get rid of the useless SessionAuth

and rearrange init Auth and App

* be more specific about which kind of user can be loaded in App

* change Update class signature

* use init.inc.php in ApiController

* don't store the whole teamconfigarr in app
  • Loading branch information
NicolasCARPi committed Aug 8, 2021
1 parent 20e785f commit 8e92afe
Show file tree
Hide file tree
Showing 83 changed files with 1,104 additions and 749 deletions.
3 changes: 2 additions & 1 deletion composer.json
Expand Up @@ -38,7 +38,8 @@
"robthree/twofactorauth": "^1.7.0",
"directorytree/ldaprecord": "^2.0.3",
"league/flysystem": "^1.1",
"league/flysystem-memory": "^1.0"
"league/flysystem-memory": "^1.0",
"lcobucci/jwt": "^4.1"
},
"require-dev": {
"roave/security-advisories": "dev-master",
Expand Down
137 changes: 136 additions & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

109 changes: 84 additions & 25 deletions src/classes/App.php
@@ -1,27 +1,35 @@
<?php
<?php declare(strict_types=1);
/**
* @package Elabftw\Elabftw
* @author Nicolas CARPi <nico-git@deltablot.email>
* @copyright 2012 Nicolas CARPi
* @license https://www.gnu.org/licenses/agpl-3.0.html AGPL-3.0
* @see https://www.elabftw.net Official website
*/
declare(strict_types=1);

namespace Elabftw\Elabftw;

use function basename;
use function bindtextdomain;
use function dirname;
use Elabftw\Exceptions\UnauthorizedException;
use Elabftw\Models\AnonymousUser;
use Elabftw\Models\AuthenticatedUser;
use Elabftw\Models\Config;
use Elabftw\Models\Teams;
use Elabftw\Models\Users;
use Elabftw\Services\Check;
use Elabftw\Traits\TwigTrait;
use Elabftw\Traits\UploadTrait;
use function in_array;
use Monolog\Handler\ErrorLogHandler;
use Monolog\Logger;
use function substr;
use function putenv;
use function setlocale;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Flash\FlashBag;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use function textdomain;

/**
* This is a super class holding various global objects
Expand All @@ -37,14 +45,16 @@ class App

public string $pageTitle = 'Lab manager';

public string $linkName = 'Documentation';

public string $linkHref = 'https://doc.elabftw.net';

public array $ok = array();

public array $ko = array();

public array $warning = array();

public array $teamConfigArr = array();

protected Db $Db;

public function __construct(public Request $Request, public SessionInterface $Session, public Config $Config, public Logger $Log, public Csrf $Csrf)
Expand All @@ -61,7 +71,7 @@ public function __construct(public Request $Request, public SessionInterface $Se
$this->Users = new Users();
$this->Db = Db::getConnection();
// UPDATE SQL SCHEMA if necessary or show error message if version mismatch
$Update = new Update($this->Config, new Sql());
$Update = new Update((int) $this->Config->configArr['schema'], new Sql());
$Update->checkSchema();
}

Expand Down Expand Up @@ -91,29 +101,39 @@ public function getMinPasswordLength(): int
return Check::MIN_PASSWORD_LENGTH;
}

/**
* If the current user is authenticated, load Users with an id
*/
public function loadUser(Users $users): void
//-*-*-*-*-*-*-**-*-*-*-*-*-*-*-//
// _ _ //
// | |__ ___ ___ | |_ //
// | '_ \ / _ \ / _ \| __| //
// | |_) | (_) | (_) | |_ //
// |_.__/ \___/ \___/ \__| //
// //
//-*-*-*-*-*-*-**-*-*-*-*-*-*-*-//
public function boot(): void
{
$this->Users = $users;

// team config
$Teams = new Teams($this->Users);
$this->teamConfigArr = $Teams->read(new ContentParams());
}
// load the Users with a userid if we are auth and not anon
if ($this->Session->has('is_auth') && $this->Session->get('userid') !== 0) {
$this->loadUser(new AuthenticatedUser(
$this->Session->get('userid'),
$this->Session->get('team'),
));
}

/**
* Get the lang (in short form like 'en' or 'fr') for the HTML attribute in head.html template
*/
public function getLangForHtmlAttribute(): string
{
$lang = 'en';
if (isset($this->Users->userData['lang'])) {
$lang = substr($this->Users->userData['lang'], 0, 2);
// ANONYMOUS
if ($this->Session->get('is_anon') === 1) {
// anon user only has access to a subset of pages
$allowedPages = array('index.php', 'experiments.php', 'database.php', 'search.php', 'make.php');
if (!in_array(basename($this->Request->getScriptName()), $allowedPages, true)) {
throw new UnauthorizedException();
}

$this->loadUser(new AnonymousUser(
$this->Session->get('team'),
$this->getLang(),
));
}

return $lang;
$this->initi18n();
}

/**
Expand All @@ -127,4 +147,43 @@ public function render(string $template, array $variables): string
{
return $this->getTwig($this->Config)->render($template, array_merge(array('App' => $this), $variables));
}

/**
* Get the lang of our current user
*/
public function getLang(): string
{
// if we have an authenticated user, use their lang setting
if ($this->Users instanceof AuthenticatedUser) {
return $this->Users->userData['lang'];
}
// default lang is the server configured one
return $this->Config->configArr['lang'];
}

/**
* Load a user object in our user field
*/
private function loadUser(AuthenticatedUser | AnonymousUser $users): void
{
$this->Users = $users;
// we have an user in a team, load the top menu link
$Teams = new Teams($this->Users);
$teamConfigArr = $Teams->read(new ContentParams());
$this->linkName = $teamConfigArr['link_name'];
$this->linkHref = $teamConfigArr['link_href'];
}

/**
* Configure gettext domain
*/
private function initi18n(): void
{
$locale = $this->getLang() . '.utf8';
$domain = 'messages';
putenv("LC_ALL=$locale");
setlocale(LC_ALL, $locale);
bindtextdomain($domain, dirname(__DIR__, 2) . '/src/langs');
textdomain($domain);
}
}

0 comments on commit 8e92afe

Please sign in to comment.