summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFrederic Guillot <fred@kanboard.net>2015-08-01 12:14:22 -0400
committerFrederic Guillot <fred@kanboard.net>2015-08-01 12:14:22 -0400
commitdb88a00d48d1dce48b8700e460c06ff7fb344f0a (patch)
treecdb089f5aadcbd22aa63612d4d50bbe63fcbe112
parentdb69d5c429cf747e72c4ded26f3821e7f688bc13 (diff)
Add bruteforce protection
-rw-r--r--ChangeLog13
-rw-r--r--app/Api/Auth.php2
-rw-r--r--app/Controller/Auth.php18
-rw-r--r--app/Model/Acl.php2
-rw-r--r--app/Model/Authentication.php117
-rw-r--r--app/Model/User.php65
-rw-r--r--app/Schema/Mysql.php8
-rw-r--r--app/Schema/Postgres.php8
-rw-r--r--app/Schema/Sqlite.php8
-rw-r--r--app/Template/auth/index.php8
-rw-r--r--app/constants.php5
-rw-r--r--composer.json5
-rw-r--r--composer.lock74
-rw-r--r--config.default.php9
-rw-r--r--docs/api-json-rpc.markdown1
-rw-r--r--docs/bruteforce-protection.markdown26
-rw-r--r--docs/config.markdown14
-rw-r--r--docs/index.markdown1
-rw-r--r--tests/units/AuthenticationTest.php39
-rw-r--r--tests/units/UserTest.php25
20 files changed, 405 insertions, 43 deletions
diff --git a/ChangeLog b/ChangeLog
index 0f62483a..de32ed58 100644
--- a/ChangeLog
+++ b/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
--------------
diff --git a/app/Api/Auth.php b/app/Api/Auth.php
index 9d401746..18fe9ff9 100644
--- a/app/Api/Auth.php
+++ b/app/Api/Auth.php
@@ -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));
}
diff --git a/app/Controller/Auth.php b/app/Controller/Auth.php
index e8889b7f..bb1154e4 100644
--- a/app/Controller/Auth.php
+++ b/app/Controller/Auth.php
@@ -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();
+ }
}
diff --git a/app/Model/Acl.php b/app/Model/Acl.php
index 95056de6..a47886bb 100644
--- a/app/Model/Acl.php
+++ b/app/Model/Acl.php
@@ -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' => '*',
diff --git a/app/Model/Authentication.php b/app/Model/Authentication.php
index 31969b57..f09312bd 100644
--- a/app/Model/Authentication.php
+++ b/app/Model/Authentication.php
@@ -5,6 +5,7 @@ namespace Model;
use Core\Request;
use SimpleValidator\Validator;
use SimpleValidator\Validators;
+use Gregwar\Captcha\CaptchaBuilder;
/**
* Authentication model
@@ -75,18 +76,52 @@ 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
*
* @access public
@@ -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']);
+ }
+ }
}
diff --git a/app/Model/User.php b/app/Model/User.php
index b6804abc..8daef3f2 100644
--- a/app/Model/User.php
+++ b/app/Model/User.php
@@ -365,6 +365,71 @@ class User extends Base
}
/**
+ * 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
*
* @access private
diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php
index 47fb806e..31ffaa32 100644
--- a/app/Schema/Mysql.php
+++ b/app/Schema/Mysql.php
@@ -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)
{
diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php
index 6b85ff57..876df981 100644
--- a/app/Schema/Postgres.php
+++ b/app/Schema/Postgres.php
@@ -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)
{
diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php
index 9e0575cf..4b8f5ac6 100644
--- a/app/Schema/Sqlite.php
+++ b/app/Schema/Sqlite.php
@@ -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)
{
diff --git a/app/Template/auth/index.php b/app/Template/auth/index.php
index ca303df9..efe95185 100644
--- a/app/Template/auth/index.php
+++ b/app/Template/auth/index.php
@@ -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">
diff --git a/app/constants.php b/app/constants.php
index 83fba468..e232aba6 100644
--- a/app/constants.php
+++ b/app/constants.php
@@ -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);
diff --git a/composer.json b/composer.json
index a6f6135e..375d2ee8 100644
--- a/composer.json
+++ b/composer.json
@@ -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" : {
diff --git a/composer.lock b/composer.lock
index 612167c1..9009a97d 100644
--- a/composer.lock
+++ b/composer.lock
@@ -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",
@@ -405,6 +405,54 @@
"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",
"source": {
@@ -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",
diff --git a/config.default.php b/config.default.php
index c392dcad..76ec38cc 100644
--- a/config.default.php
+++ b/config.default.php
@@ -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);
diff --git a/docs/api-json-rpc.markdown b/docs/api-json-rpc.markdown
index 9c572304..7c211081 100644
--- a/docs/api-json-rpc.markdown
+++ b/docs/api-json-rpc.markdown
@@ -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
diff --git a/docs/bruteforce-protection.markdown b/docs/bruteforce-protection.markdown
new file mode 100644
index 00000000..633cfe87
--- /dev/null
+++ b/docs/bruteforce-protection.markdown
@@ -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);
+```
diff --git a/docs/config.markdown b/docs/config.markdown
index 45ba7a91..44f72b32 100644
--- a/docs/config.markdown
+++ b/docs/config.markdown
@@ -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
----------------
diff --git a/docs/index.markdown b/docs/index.markdown
index 5ef523f9..181e1d03 100644
--- a/docs/index.markdown
+++ b/docs/index.markdown
@@ -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
diff --git a/tests/units/AuthenticationTest.php b/tests/units/AuthenticationTest.php
new file mode 100644
index 00000000..75b55ece
--- /dev/null
+++ b/tests/units/AuthenticationTest.php
@@ -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'));
+ }
+}
diff --git a/tests/units/UserTest.php b/tests/units/UserTest.php
index fcdf3934..6c68dfd2 100644
--- a/tests/units/UserTest.php
+++ b/tests/units/UserTest.php
@@ -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);