Skip to content

Commit 3fddcc0

Browse files
committed
User: Add login_max_attempt_before_blocking_account configuration setting - refs BT#20083
Block a user account if there are multiple failed login attempts. It requires DB changes: ```sql CREATE TABLE track_e_login_attempt ( login_id INT AUTO_INCREMENT NOT NULL, username VARCHAR(100) NOT NULL, login_date DATETIME NOT NULL, user_ip VARCHAR(39) NOT NULL, success TINYINT(1) NOT NULL, INDEX idx_track_e_login_attempt_username_success (username, success), PRIMARY KEY (login_id) ) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB; ``` Then add the "@" symbol to TrackELoginAttempt` class in the `ORM\Entity()` line.
1 parent 88ac26d commit 3fddcc0

File tree

5 files changed

+224
-2
lines changed

5 files changed

+224
-2
lines changed

main/inc/lib/events.lib.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
/* See license terms in /license.txt */
44

55
use Chamilo\CoreBundle\Component\Utils\ChamiloApi;
6+
use Chamilo\CoreBundle\Entity\TrackELoginAttempt;
67
use ChamiloSession as Session;
78

89
/**
@@ -52,6 +53,28 @@ public static function open()
5253
return 1;
5354
}
5455

56+
/**
57+
* @throws Exception
58+
*/
59+
public static function eventLoginAttempt(string $username, bool $success = false)
60+
{
61+
if ((int) api_get_configuration_value('login_max_attempt_before_blocking_account') <= 0) {
62+
return;
63+
}
64+
65+
$attempt = new TrackELoginAttempt();
66+
$attempt
67+
->setUsername($username)
68+
->setLoginDate(api_get_utc_datetime(null, false, true))
69+
->setUserIp(api_get_real_ip())
70+
->setSuccess($success)
71+
;
72+
73+
$em = Database::getManager();
74+
$em->persist($attempt);
75+
$em->flush();
76+
}
77+
5578
/**
5679
* @author Sebastien Piraux <[email protected]> old code
5780
* @author Julio Montoya

main/inc/lib/usermanager.lib.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Chamilo\CoreBundle\Entity\Session as SessionEntity;
88
use Chamilo\CoreBundle\Entity\SkillRelUser;
99
use Chamilo\CoreBundle\Entity\SkillRelUserComment;
10+
use Chamilo\CoreBundle\Entity\TrackELoginAttempt;
1011
use Chamilo\UserBundle\Entity\User;
1112
use Chamilo\UserBundle\Repository\UserRepository;
1213
use ChamiloSession as Session;
@@ -6809,6 +6810,59 @@ public static function logInAsFirstAdmin()
68096810
return [];
68106811
}
68116812

6813+
public static function blockIfMaxLoginAttempts(array $userInfo)
6814+
{
6815+
if (false === (bool) $userInfo['active'] || null === $userInfo['last_login']) {
6816+
return;
6817+
}
6818+
6819+
$maxAllowed = (int) api_get_configuration_value('login_max_attempt_before_blocking_account');
6820+
6821+
if ($maxAllowed <= 0) {
6822+
return;
6823+
}
6824+
6825+
$em = Database::getManager();
6826+
6827+
$countFailedAttempts = $em
6828+
->getRepository(TrackELoginAttempt::class)
6829+
->createQueryBuilder('la')
6830+
->select('COUNT(la)')
6831+
->where('la.username = :username')
6832+
->andWhere('la.loginDate >= :last_login')
6833+
->andWhere('la.success <> TRUE')
6834+
->setParameters(
6835+
[
6836+
'username' => $userInfo['username'],
6837+
'last_login' => $userInfo['last_login'],
6838+
]
6839+
)
6840+
->getQuery()
6841+
->getSingleScalarResult()
6842+
;
6843+
6844+
if ($countFailedAttempts >= $maxAllowed) {
6845+
Database::update(
6846+
Database::get_main_table(TABLE_MAIN_USER),
6847+
['active' => false],
6848+
['username = ?' => $userInfo['username']]
6849+
);
6850+
//XAccountDisabledByYAttempts
6851+
6852+
Display::addFlash(
6853+
Display::return_message(
6854+
sprintf(
6855+
get_lang('The account for username <i>%s</i> was disabled after %d failed login attempts.'),
6856+
$userInfo['username'],
6857+
$countFailedAttempts
6858+
),
6859+
'error',
6860+
false
6861+
)
6862+
);
6863+
}
6864+
}
6865+
68126866
/**
68136867
* Check if user is teacher of a student based in their courses.
68146868
*

main/inc/local.inc.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,7 @@
449449

450450
// Lookup the user in the main database
451451
$user_table = Database::get_main_table(TABLE_MAIN_USER);
452-
$sql = "SELECT user_id, username, password, auth_source, active, expiration_date, status, salt
452+
$sql = "SELECT user_id, username, password, auth_source, active, expiration_date, status, salt, last_login
453453
FROM $user_table
454454
WHERE username = '".Database::escape_string($login)."'";
455455
$result = Database::query($sql);
@@ -608,13 +608,15 @@
608608
$_user['user_id'] = $uData['user_id'];
609609
$_user['status'] = $uData['status'];
610610
Session::write('_user', $_user);
611+
Event::eventLoginAttempt($uData['username'], true);
611612
Event::eventLogin($_user['user_id']);
612613
$logging_in = true;
613614
} else {
614615
$loginFailed = true;
615616
Session::erase('_uid');
616617
Session::write('loginFailed', '1');
617-
618+
Event::eventLoginAttempt($uData['username']);
619+
UserManager::blockIfMaxLoginAttempts($uData);
618620
// Fix cas redirection loop
619621
// https://support.chamilo.org/issues/6124
620622
$location = api_get_path(WEB_PATH)
@@ -631,6 +633,7 @@
631633
$_user['status'] = $uData['status'];
632634
Session::write('_user', $_user);
633635
Event::eventLogin($_user['user_id']);
636+
Event::eventLoginAttempt($uData['username'], true);
634637
$logging_in = true;
635638
} else {
636639
//This means a secondary admin wants to login so we check as he's a normal user
@@ -640,11 +643,14 @@
640643
$_user['status'] = $uData['status'];
641644
Session::write('_user', $_user);
642645
Event::eventLogin($_user['user_id']);
646+
Event::eventLoginAttempt($uData['username'], true);
643647
$logging_in = true;
644648
} else {
645649
$loginFailed = true;
646650
Session::erase('_uid');
647651
Session::write('loginFailed', '1');
652+
Event::eventLoginAttempt($uData['username']);
653+
UserManager::blockIfMaxLoginAttempts($uData);
648654
header(
649655
'Location: '.api_get_path(WEB_PATH)
650656
.'index.php?loginFailed=1&error=access_url_inactive'
@@ -661,6 +667,7 @@
661667

662668
Session::write('_user', $_user);
663669
Event::eventLogin($uData['user_id']);
670+
Event::eventLoginAttempt($uData['username'], true);
664671
$logging_in = true;
665672
}
666673
} else {
@@ -688,6 +695,8 @@
688695
$loginFailed = true;
689696
Session::erase('_uid');
690697
Session::write('loginFailed', '1');
698+
Event::eventLoginAttempt($uData['username']);
699+
UserManager::blockIfMaxLoginAttempts($uData);
691700

692701
if ($allowCaptcha) {
693702
if (isset($_SESSION['loginFailedCount'])) {
@@ -768,6 +777,9 @@
768777
);
769778
}
770779
} else {
780+
Event::eventLoginAttempt($login);
781+
UserManager::blockIfMaxLoginAttempts(['username' => $login, 'last_login' => null]);
782+
771783
$extraFieldValue = new ExtraFieldValue('user');
772784
$uData = $extraFieldValue->get_item_id_from_field_variable_and_field_value(
773785
'organisationemail',

main/install/configuration.dist.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2091,6 +2091,24 @@
20912091
// Enable admin-only APIs: get_users_api_keys, get_user_api_key
20922092
//$_configuration['webservice_enable_adminonly_api'] = false;
20932093

2094+
// Block a user account if there are multiple failed login attempts. It requires DB changes:
2095+
/*
2096+
CREATE TABLE track_e_login_attempt
2097+
(
2098+
login_id INT AUTO_INCREMENT NOT NULL,
2099+
username VARCHAR(100) NOT NULL,
2100+
login_date DATETIME NOT NULL,
2101+
user_ip VARCHAR(39) NOT NULL,
2102+
success TINYINT(1) NOT NULL,
2103+
INDEX idx_track_e_login_attempt_username_success (username, success),
2104+
PRIMARY KEY (login_id)
2105+
) DEFAULT CHARACTER SET utf8
2106+
COLLATE utf8_unicode_ci
2107+
ENGINE = InnoDB;
2108+
*/
2109+
// Then add the "@" symbol to TrackELoginAttempt class in the ORM\Entity() line.
2110+
//$_configuration['login_max_attempt_before_blocking_account'] = 0;
2111+
20942112
// Ask user to renew password at first login.
20952113
// Requires a user checkbox extra field called "ask_new_password".
20962114
//$_configuration['force_renew_password_at_first_login'] = true;
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
<?php
2+
3+
/* For licensing terms, see /license.txt */
4+
5+
namespace Chamilo\CoreBundle\Entity;
6+
7+
use Doctrine\ORM\Mapping as ORM;
8+
9+
/**
10+
* @ORM\Table(
11+
* name="track_e_login_attempt",
12+
* indexes={
13+
* @ORM\Index(name="idx_track_e_login_attempt_username_success", columns={"username", "success"})
14+
* }
15+
* )
16+
* Add @ to the next line if api_get_configuration_value('login_max_attempt_before_blocking_account') is enabled.
17+
* ORM\Entity
18+
*/
19+
class TrackELoginAttempt
20+
{
21+
/**
22+
* @var int
23+
*
24+
* @ORM\Column(name="login_id", type="integer")
25+
* @ORM\Id
26+
* @ORM\GeneratedValue(strategy="IDENTITY")
27+
*/
28+
protected $id;
29+
30+
/**
31+
* @var string
32+
*
33+
* @ORM\Column(name="username", type="string", length=100)
34+
*/
35+
protected $username;
36+
37+
/**
38+
* @var \DateTime
39+
*
40+
* @ORM\Column(name="login_date", type="datetime")
41+
*/
42+
protected $loginDate;
43+
44+
/**
45+
* @var string
46+
*
47+
* @ORM\Column(name="user_ip", type="string", length=39)
48+
*/
49+
protected $userIp;
50+
51+
/**
52+
* @var bool
53+
*
54+
* @ORM\Column(name="success", type="boolean")
55+
*/
56+
protected $success;
57+
58+
public function __construct()
59+
{
60+
$this->success = false;
61+
}
62+
63+
public function getId(): int
64+
{
65+
return $this->id;
66+
}
67+
68+
public function getUsername(): string
69+
{
70+
return $this->username;
71+
}
72+
73+
public function setUsername(string $username): TrackELoginAttempt
74+
{
75+
$this->username = $username;
76+
77+
return $this;
78+
}
79+
80+
public function getLoginDate(): \DateTime
81+
{
82+
return $this->loginDate;
83+
}
84+
85+
public function setLoginDate(\DateTime $loginDate): TrackELoginAttempt
86+
{
87+
$this->loginDate = $loginDate;
88+
89+
return $this;
90+
}
91+
92+
public function getUserIp(): string
93+
{
94+
return $this->userIp;
95+
}
96+
97+
public function setUserIp(string $userIp): TrackELoginAttempt
98+
{
99+
$this->userIp = $userIp;
100+
101+
return $this;
102+
}
103+
104+
public function isSuccess(): bool
105+
{
106+
return $this->success;
107+
}
108+
109+
public function setSuccess(bool $success): TrackELoginAttempt
110+
{
111+
$this->success = $success;
112+
113+
return $this;
114+
}
115+
}

0 commit comments

Comments
 (0)