Add bruteforce protection
This commit is contained in:
parent
db69d5c429
commit
db88a00d48
13
ChangeLog
13
ChangeLog
|
|
@ -1,3 +1,16 @@
|
|||
Version 1.0.18 (unreleased)
|
||||
---------------------------
|
||||
|
||||
New features:
|
||||
|
||||
* Add login bruteforce protection with captcha and account lockdown
|
||||
* Add new api procedures: getDefaultTaskColor(), getDefaultTaskColors() and getColorList()
|
||||
* Add user api access
|
||||
|
||||
Bug fixes:
|
||||
|
||||
* Wrong template name for subtasks tooltip due to previous refactoring
|
||||
|
||||
Version 1.0.17
|
||||
--------------
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class Auth extends Base
|
|||
{
|
||||
$this->container['dispatcher']->dispatch('api.bootstrap', new Event);
|
||||
|
||||
if ($username !== 'jsonrpc' && $this->authentication->authenticate($username, $password)) {
|
||||
if ($username !== 'jsonrpc' && ! $this->authentication->hasCaptcha($username) && $this->authentication->authenticate($username, $password)) {
|
||||
$this->checkProcedurePermission(true, $method);
|
||||
$this->userSession->refresh($this->user->getByUsername($username));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace Controller;
|
||||
|
||||
use Gregwar\Captcha\CaptchaBuilder;
|
||||
|
||||
/**
|
||||
* Authentication controller
|
||||
*
|
||||
|
|
@ -22,6 +24,7 @@ class Auth extends Base
|
|||
}
|
||||
|
||||
$this->response->html($this->template->layout('auth/index', array(
|
||||
'captcha' => isset($values['username']) && $this->authentication->hasCaptcha($values['username']),
|
||||
'errors' => $errors,
|
||||
'values' => $values,
|
||||
'no_layout' => true,
|
||||
|
|
@ -64,4 +67,19 @@ class Auth extends Base
|
|||
$this->session->close();
|
||||
$this->response->redirect($this->helper->url->to('auth', 'login'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display captcha image
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function captcha()
|
||||
{
|
||||
$this->response->contentType('image/jpeg');
|
||||
|
||||
$builder = new CaptchaBuilder;
|
||||
$builder->build();
|
||||
$this->session['captcha'] = $builder->getPhrase();
|
||||
$builder->output();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class Acl extends Base
|
|||
* @var array
|
||||
*/
|
||||
private $public_acl = array(
|
||||
'auth' => array('login', 'check'),
|
||||
'auth' => array('login', 'check', 'captcha'),
|
||||
'task' => array('readonly'),
|
||||
'board' => array('readonly'),
|
||||
'webhook' => '*',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ namespace Model;
|
|||
use Core\Request;
|
||||
use SimpleValidator\Validator;
|
||||
use SimpleValidator\Validators;
|
||||
use Gregwar\Captcha\CaptchaBuilder;
|
||||
|
||||
/**
|
||||
* Authentication model
|
||||
|
|
@ -75,17 +76,51 @@ class Authentication extends Base
|
|||
*/
|
||||
public function authenticate($username, $password)
|
||||
{
|
||||
// Try first the database auth and then LDAP if activated
|
||||
if ($this->backend('database')->authenticate($username, $password)) {
|
||||
if ($this->user->isLocked($username)) {
|
||||
$this->container['logger']->error('Account locked: '.$username);
|
||||
return false;
|
||||
}
|
||||
else if ($this->backend('database')->authenticate($username, $password)) {
|
||||
$this->user->resetFailedLogin($username);
|
||||
return true;
|
||||
}
|
||||
else if (LDAP_AUTH && $this->backend('ldap')->authenticate($username, $password)) {
|
||||
$this->user->resetFailedLogin($username);
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->handleFailedLogin($username);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the captcha must be shown
|
||||
*
|
||||
* @access public
|
||||
* @param string $username
|
||||
* @return boolean
|
||||
*/
|
||||
public function hasCaptcha($username)
|
||||
{
|
||||
return $this->user->getFailedLogin($username) >= BRUTEFORCE_CAPTCHA;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle failed login
|
||||
*
|
||||
* @access public
|
||||
* @param string $username
|
||||
*/
|
||||
public function handleFailedLogin($username)
|
||||
{
|
||||
$this->user->incrementFailedLogin($username);
|
||||
|
||||
if ($this->user->getFailedLogin($username) >= BRUTEFORCE_LOCKDOWN) {
|
||||
$this->container['logger']->critical('Locking account: '.$username);
|
||||
$this->user->lock($username, BRUTEFORCE_LOCKDOWN_DURATION);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate user login form
|
||||
*
|
||||
|
|
@ -95,27 +130,12 @@ class Authentication extends Base
|
|||
*/
|
||||
public function validateForm(array $values)
|
||||
{
|
||||
$v = new Validator($values, array(
|
||||
new Validators\Required('username', t('The username is required')),
|
||||
new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50),
|
||||
new Validators\Required('password', t('The password is required')),
|
||||
));
|
||||
|
||||
$result = $v->execute();
|
||||
$errors = $v->getErrors();
|
||||
list($result, $errors) = $this->validateFormCredentials($values);
|
||||
|
||||
if ($result) {
|
||||
|
||||
if ($this->authenticate($values['username'], $values['password'])) {
|
||||
|
||||
// Setup the remember me feature
|
||||
if (! empty($values['remember_me'])) {
|
||||
|
||||
$credentials = $this->backend('rememberMe')
|
||||
->create($this->userSession->getId(), Request::getIpAddress(), Request::getUserAgent());
|
||||
|
||||
$this->backend('rememberMe')->writeCookie($credentials['token'], $credentials['sequence'], $credentials['expiration']);
|
||||
}
|
||||
if ($this->validateFormCaptcha($values) && $this->authenticate($values['username'], $values['password'])) {
|
||||
$this->createRememberMeSession($values);
|
||||
}
|
||||
else {
|
||||
$result = false;
|
||||
|
|
@ -123,9 +143,62 @@ class Authentication extends Base
|
|||
}
|
||||
}
|
||||
|
||||
return array($result, $errors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate credentials syntax
|
||||
*
|
||||
* @access public
|
||||
* @param array $values Form values
|
||||
* @return array $valid, $errors [0] = Success or not, [1] = List of errors
|
||||
*/
|
||||
public function validateFormCredentials(array $values)
|
||||
{
|
||||
$v = new Validator($values, array(
|
||||
new Validators\Required('username', t('The username is required')),
|
||||
new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50),
|
||||
new Validators\Required('password', t('The password is required')),
|
||||
));
|
||||
|
||||
return array(
|
||||
$result,
|
||||
$errors
|
||||
$v->execute(),
|
||||
$v->getErrors(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate captcha
|
||||
*
|
||||
* @access public
|
||||
* @param array $values Form values
|
||||
* @return boolean
|
||||
*/
|
||||
public function validateFormCaptcha(array $values)
|
||||
{
|
||||
if ($this->hasCaptcha($values['username'])) {
|
||||
$builder = new CaptchaBuilder;
|
||||
$builder->setPhrase($this->session['captcha']);
|
||||
return $builder->testPhrase(isset($values['captcha']) ? $values['captcha'] : '');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create remember me session if necessary
|
||||
*
|
||||
* @access private
|
||||
* @param array $values Form values
|
||||
*/
|
||||
private function createRememberMeSession(array $values)
|
||||
{
|
||||
if (! empty($values['remember_me'])) {
|
||||
|
||||
$credentials = $this->backend('rememberMe')
|
||||
->create($this->userSession->getId(), Request::getIpAddress(), Request::getUserAgent());
|
||||
|
||||
$this->backend('rememberMe')->writeCookie($credentials['token'], $credentials['sequence'], $credentials['expiration']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -364,6 +364,71 @@ class User extends Base
|
|||
->save(array('token' => ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of failed login for the user
|
||||
*
|
||||
* @access public
|
||||
* @param string $username
|
||||
* @return integer
|
||||
*/
|
||||
public function getFailedLogin($username)
|
||||
{
|
||||
return (int) $this->db->table(self::TABLE)->eq('username', $username)->findOneColumn('nb_failed_login');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset to 0 the counter of failed login
|
||||
*
|
||||
* @access public
|
||||
* @param string $username
|
||||
* @return boolean
|
||||
*/
|
||||
public function resetFailedLogin($username)
|
||||
{
|
||||
return $this->db->table(self::TABLE)->eq('username', $username)->update(array('nb_failed_login' => 0, 'lock_expiration_date' => 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment failed login counter
|
||||
*
|
||||
* @access public
|
||||
* @param string $username
|
||||
* @return boolean
|
||||
*/
|
||||
public function incrementFailedLogin($username)
|
||||
{
|
||||
return $this->db->execute('UPDATE '.self::TABLE.' SET nb_failed_login=nb_failed_login+1 WHERE username=?', array($username)) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the account is locked
|
||||
*
|
||||
* @access public
|
||||
* @param string $username
|
||||
* @return boolean
|
||||
*/
|
||||
public function isLocked($username)
|
||||
{
|
||||
return $this->db->table(self::TABLE)
|
||||
->eq('username', $username)
|
||||
->neq('lock_expiration_date', 0)
|
||||
->gte('lock_expiration_date', time())
|
||||
->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock the account for the specified duration
|
||||
*
|
||||
* @access public
|
||||
* @param string $username Username
|
||||
* @param integer $duration Duration in minutes
|
||||
* @return boolean
|
||||
*/
|
||||
public function lock($username, $duration = 15)
|
||||
{
|
||||
return $this->db->table(self::TABLE)->eq('username', $username)->update(array('lock_expiration_date' => time() + $duration * 60));
|
||||
}
|
||||
|
||||
/**
|
||||
* Common validation rules
|
||||
*
|
||||
|
|
|
|||
|
|
@ -6,7 +6,13 @@ use PDO;
|
|||
use Core\Security;
|
||||
use Model\Link;
|
||||
|
||||
const VERSION = 81;
|
||||
const VERSION = 82;
|
||||
|
||||
function version_82($pdo)
|
||||
{
|
||||
$pdo->exec("ALTER TABLE users ADD COLUMN nb_failed_login INT DEFAULT 0");
|
||||
$pdo->exec("ALTER TABLE users ADD COLUMN lock_expiration_date INT DEFAULT 0");
|
||||
}
|
||||
|
||||
function version_81($pdo)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,7 +6,13 @@ use PDO;
|
|||
use Core\Security;
|
||||
use Model\Link;
|
||||
|
||||
const VERSION = 61;
|
||||
const VERSION = 62;
|
||||
|
||||
function version_62($pdo)
|
||||
{
|
||||
$pdo->exec("ALTER TABLE users ADD COLUMN nb_failed_login INTEGER DEFAULT 0");
|
||||
$pdo->exec("ALTER TABLE users ADD COLUMN lock_expiration_date INTEGER DEFAULT 0");
|
||||
}
|
||||
|
||||
function version_61($pdo)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,7 +6,13 @@ use Core\Security;
|
|||
use PDO;
|
||||
use Model\Link;
|
||||
|
||||
const VERSION = 77;
|
||||
const VERSION = 78;
|
||||
|
||||
function version_78($pdo)
|
||||
{
|
||||
$pdo->exec("ALTER TABLE users ADD COLUMN nb_failed_login INTEGER DEFAULT 0");
|
||||
$pdo->exec("ALTER TABLE users ADD COLUMN lock_expiration_date INTEGER DEFAULT 0");
|
||||
}
|
||||
|
||||
function version_77($pdo)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -10,11 +10,17 @@
|
|||
<?= $this->form->csrf() ?>
|
||||
|
||||
<?= $this->form->label(t('Username'), 'username') ?>
|
||||
<?= $this->form->text('username', $values, $errors, array('autofocus', 'required')) ?><br/>
|
||||
<?= $this->form->text('username', $values, $errors, array('autofocus', 'required')) ?>
|
||||
|
||||
<?= $this->form->label(t('Password'), 'password') ?>
|
||||
<?= $this->form->password('password', $values, $errors, array('required')) ?>
|
||||
|
||||
<?php if ($captcha): ?>
|
||||
<?= $this->form->label(t('Enter the text below'), 'captcha') ?>
|
||||
<img src="<?= $this->url->href('auth', 'captcha') ?>"/>
|
||||
<?= $this->form->text('captcha', $values, $errors, array('required')) ?>
|
||||
<?php endif ?>
|
||||
|
||||
<?= $this->form->checkbox('remember_me', t('Remember Me'), 1, true) ?><br/>
|
||||
|
||||
<div class="form-actions">
|
||||
|
|
|
|||
|
|
@ -88,3 +88,8 @@ defined('ENABLE_URL_REWRITE') or define('ENABLE_URL_REWRITE', isset($_SERVER['HT
|
|||
|
||||
// Hide login form
|
||||
defined('HIDE_LOGIN_FORM') or define('HIDE_LOGIN_FORM', false);
|
||||
|
||||
// Bruteforce protection
|
||||
defined('BRUTEFORCE_CAPTCHA') or define('BRUTEFORCE_CAPTCHA', 3);
|
||||
defined('BRUTEFORCE_LOCKDOWN') or define('BRUTEFORCE_LOCKDOWN', 6);
|
||||
defined('BRUTEFORCE_LOCKDOWN_DURATION') or define('BRUTEFORCE_LOCKDOWN_DURATION', 15);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"ext-mbstring" : "*",
|
||||
"ext-gd" : "*",
|
||||
"christian-riesen/otp" : "1.4",
|
||||
"eluceo/ical": "*",
|
||||
"eluceo/ical": "0.7.0",
|
||||
"erusev/parsedown" : "1.5.3",
|
||||
"fabiang/xmpp" : "0.6.1",
|
||||
"fguillot/json-rpc" : "dev-master",
|
||||
|
|
@ -15,7 +15,8 @@
|
|||
"pimple/pimple" : "~3.0",
|
||||
"swiftmailer/swiftmailer" : "@stable",
|
||||
"symfony/console" : "@stable",
|
||||
"symfony/event-dispatcher" : "~2.6"
|
||||
"symfony/event-dispatcher" : "~2.6",
|
||||
"gregwar/captcha": "1.*"
|
||||
},
|
||||
"autoload" : {
|
||||
"psr-0" : {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"hash": "1c0cc116db3d03c38df0f0efa59e9df7",
|
||||
"hash": "d88040d41c5a7cfee8e6fcd9dbc4a591",
|
||||
"packages": [
|
||||
{
|
||||
"name": "christian-riesen/base32",
|
||||
|
|
@ -404,6 +404,54 @@
|
|||
"homepage": "https://github.com/fguillot/simpleLogger",
|
||||
"time": "2015-05-30 19:25:09"
|
||||
},
|
||||
{
|
||||
"name": "gregwar/captcha",
|
||||
"version": "v1.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Gregwar/Captcha.git",
|
||||
"reference": "9caea9eb001a809ab8734b201ae07ebe4179238d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Gregwar/Captcha/zipball/9caea9eb001a809ab8734b201ae07ebe4179238d",
|
||||
"reference": "9caea9eb001a809ab8734b201ae07ebe4179238d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-gd": "*",
|
||||
"php": ">=5.3.0"
|
||||
},
|
||||
"type": "captcha",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Gregwar\\Captcha\\": "/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Grégoire Passault",
|
||||
"email": "g.passault@gmail.com",
|
||||
"homepage": "http://www.gregwar.com/"
|
||||
},
|
||||
{
|
||||
"name": "Jeremy Livingston",
|
||||
"email": "jeremy.j.livingston@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "Captcha generator",
|
||||
"homepage": "https://github.com/Gregwar/Captcha",
|
||||
"keywords": [
|
||||
"bot",
|
||||
"captcha",
|
||||
"spam"
|
||||
],
|
||||
"time": "2015-05-13 06:34:33"
|
||||
},
|
||||
{
|
||||
"name": "nickcernis/html-to-markdown",
|
||||
"version": "2.2.1",
|
||||
|
|
@ -452,16 +500,16 @@
|
|||
},
|
||||
{
|
||||
"name": "pimple/pimple",
|
||||
"version": "v3.0.0",
|
||||
"version": "v3.0.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/silexphp/Pimple.git",
|
||||
"reference": "876bf0899d01feacd2a2e83f04641e51350099ef"
|
||||
"reference": "3313af5935dbc560fab845b76a1ca351b47855af"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/silexphp/Pimple/zipball/876bf0899d01feacd2a2e83f04641e51350099ef",
|
||||
"reference": "876bf0899d01feacd2a2e83f04641e51350099ef",
|
||||
"url": "https://api.github.com/repos/silexphp/Pimple/zipball/3313af5935dbc560fab845b76a1ca351b47855af",
|
||||
"reference": "3313af5935dbc560fab845b76a1ca351b47855af",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -494,7 +542,7 @@
|
|||
"container",
|
||||
"dependency injection"
|
||||
],
|
||||
"time": "2014-07-24 09:48:15"
|
||||
"time": "2015-07-30 09:57:46"
|
||||
},
|
||||
{
|
||||
"name": "psr/log",
|
||||
|
|
@ -589,16 +637,16 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/console",
|
||||
"version": "v2.7.2",
|
||||
"version": "v2.7.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/Console.git",
|
||||
"reference": "8cf484449130cabfd98dcb4694ca9945802a21ed"
|
||||
"reference": "d6cf02fe73634c96677e428f840704bfbcaec29e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/Console/zipball/8cf484449130cabfd98dcb4694ca9945802a21ed",
|
||||
"reference": "8cf484449130cabfd98dcb4694ca9945802a21ed",
|
||||
"url": "https://api.github.com/repos/symfony/Console/zipball/d6cf02fe73634c96677e428f840704bfbcaec29e",
|
||||
"reference": "d6cf02fe73634c96677e428f840704bfbcaec29e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -642,11 +690,11 @@
|
|||
],
|
||||
"description": "Symfony Console Component",
|
||||
"homepage": "https://symfony.com",
|
||||
"time": "2015-07-09 16:07:40"
|
||||
"time": "2015-07-28 15:18:12"
|
||||
},
|
||||
{
|
||||
"name": "symfony/event-dispatcher",
|
||||
"version": "v2.7.2",
|
||||
"version": "v2.7.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/EventDispatcher.git",
|
||||
|
|
@ -706,7 +754,7 @@
|
|||
"packages-dev": [
|
||||
{
|
||||
"name": "symfony/stopwatch",
|
||||
"version": "v2.7.2",
|
||||
"version": "v2.7.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/Stopwatch.git",
|
||||
|
|
|
|||
|
|
@ -159,3 +159,12 @@ define('ENABLE_URL_REWRITE', false);
|
|||
|
||||
// Hide login form, useful if all your users use Google/Github/ReverseProxy authentication
|
||||
define('HIDE_LOGIN_FORM', false);
|
||||
|
||||
// Enable captcha after 3 authentication failure
|
||||
define('BRUTEFORCE_CAPTCHA', 3);
|
||||
|
||||
// Lock the account after 6 authentication failure
|
||||
define('BRUTEFORCE_LOCKDOWN', 6);
|
||||
|
||||
// Lock account duration in minute
|
||||
define('BRUTEFORCE_LOCKDOWN_DURATION', 15);
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ Security
|
|||
|
||||
- Always use HTTPS with a valid certificate
|
||||
- If you make a mobile application, it's your job to store securely the user credentials on the device
|
||||
- After 3 authentication failure on the user api, the end-user have to unlock his account by using the login form
|
||||
- Two factor authentication is not yet available through the API
|
||||
|
||||
Protocol
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
Bruteforce Protection
|
||||
=====================
|
||||
|
||||
The brute force protection of Kanboard works at the user account level:
|
||||
|
||||
- After 3 authentication failure for the same username, the login form show a captcha image to prevent automated bot tentatives.
|
||||
- After 6 authentication failure, the user account is locked down for a period of 15 minutes.
|
||||
|
||||
This feature works only for authentication methods that use the login form.
|
||||
|
||||
However, **after 3 authentication failure through the user API**, the account have to be unlocked by using the login form.
|
||||
|
||||
Kanboard doesn't block any IP addresses since bots can use several anonymous proxies. However, you can use external tools like [fail2ban](http://www.fail2ban.org) to avoid massive scans.
|
||||
|
||||
Default settings can be changed with these configuration variables:
|
||||
|
||||
```php
|
||||
// Enable captcha after 3 authentication failure
|
||||
define('BRUTEFORCE_CAPTCHA', 3);
|
||||
|
||||
// Lock the account after 6 authentication failure
|
||||
define('BRUTEFORCE_LOCKDOWN', 6);
|
||||
|
||||
// Lock account duration in minute
|
||||
define('BRUTEFORCE_LOCKDOWN_DURATION', 15);
|
||||
```
|
||||
|
|
@ -196,6 +196,20 @@ define('ENABLE_HSTS', true);
|
|||
define('ENABLE_XFRAME', true);
|
||||
```
|
||||
|
||||
Bruteforce protection
|
||||
---------------------
|
||||
|
||||
```php
|
||||
// Enable captcha after 3 authentication failure
|
||||
define('BRUTEFORCE_CAPTCHA', 3);
|
||||
|
||||
// Lock the account after 6 authentication failure
|
||||
define('BRUTEFORCE_LOCKDOWN', 6);
|
||||
|
||||
// Lock account duration in minute
|
||||
define('BRUTEFORCE_LOCKDOWN_DURATION', 15);
|
||||
```
|
||||
|
||||
Various settings
|
||||
----------------
|
||||
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ Using Kanboard
|
|||
- [Advanced Search Syntax](search.markdown)
|
||||
- [Command line interface](cli.markdown)
|
||||
- [Syntax guide](syntax-guide.markdown)
|
||||
- [Bruteforce protection](bruteforce-protection.markdown)
|
||||
- [Frequently asked questions](faq.markdown)
|
||||
|
||||
Technical details
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
require_once __DIR__.'/Base.php';
|
||||
|
||||
use Model\User;
|
||||
use Model\Authentication;
|
||||
|
||||
class AuthenticationTest extends Base
|
||||
{
|
||||
public function testHasCaptcha()
|
||||
{
|
||||
$u = new User($this->container);
|
||||
$a = new Authentication($this->container);
|
||||
|
||||
$this->assertFalse($a->hasCaptcha('not_found'));
|
||||
$this->assertFalse($a->hasCaptcha('admin'));
|
||||
|
||||
$this->assertTrue($u->incrementFailedLogin('admin'));
|
||||
$this->assertTrue($u->incrementFailedLogin('admin'));
|
||||
$this->assertTrue($u->incrementFailedLogin('admin'));
|
||||
|
||||
$this->assertFalse($a->hasCaptcha('not_found'));
|
||||
$this->assertTrue($a->hasCaptcha('admin'));
|
||||
}
|
||||
|
||||
public function testHandleFailedLogin()
|
||||
{
|
||||
$u = new User($this->container);
|
||||
$a = new Authentication($this->container);
|
||||
|
||||
$this->assertFalse($u->isLocked('admin'));
|
||||
|
||||
for ($i = 0; $i <= 6; $i++) {
|
||||
$a->handleFailedLogin('admin');
|
||||
}
|
||||
|
||||
$this->assertTrue($u->isLocked('admin'));
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,31 @@ use Model\Project;
|
|||
|
||||
class UserTest extends Base
|
||||
{
|
||||
public function testFailedLogin()
|
||||
{
|
||||
$u = new User($this->container);
|
||||
|
||||
$this->assertEquals(0, $u->getFailedLogin('admin'));
|
||||
$this->assertEquals(0, $u->getFailedLogin('not_found'));
|
||||
|
||||
$this->assertTrue($u->incrementFailedLogin('admin'));
|
||||
$this->assertTrue($u->incrementFailedLogin('admin'));
|
||||
|
||||
$this->assertEquals(2, $u->getFailedLogin('admin'));
|
||||
$this->assertTrue($u->resetFailedLogin('admin'));
|
||||
$this->assertEquals(0, $u->getFailedLogin('admin'));
|
||||
}
|
||||
|
||||
public function testLocking()
|
||||
{
|
||||
$u = new User($this->container);
|
||||
|
||||
$this->assertFalse($u->isLocked('admin'));
|
||||
$this->assertFalse($u->isLocked('not_found'));
|
||||
$this->assertTrue($u->lock('admin', 1));
|
||||
$this->assertTrue($u->isLocked('admin'));
|
||||
}
|
||||
|
||||
public function testGetByEmail()
|
||||
{
|
||||
$u = new User($this->container);
|
||||
|
|
|
|||
Loading…
Reference in New Issue