Skip to content

Commit d4250cf

Browse files
committed
Disable account after failed login attempts
Signed-off-by: Roeland Jago Douma <roeland@famdouma.nl>
1 parent b617166 commit d4250cf

9 files changed

Lines changed: 217 additions & 1 deletion

File tree

appinfo/info.xml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@
66
<description>
77
Allow admin to define certain pre-conditions for password, e.g. enforce a minimum length
88
</description>
9-
<version>1.9.0</version>
9+
<version>1.9.1</version>
1010
<licence>agpl</licence>
1111
<author>Bjoern Schiessle</author>
1212
<namespace>Password_Policy</namespace>
13+
<types>
14+
<authentication/>
15+
</types>
1316
<default_enable/>
1417
<category>security</category>
1518
<bugs>https://github.com/nextcloud/password_policy/issues</bugs>

js/settings-admin.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ $(document).ready(function(){
110110
elem: '#password-policy-expiration',
111111
conf: 'expiration',
112112
},
113+
{
114+
elem: '#password-policy-failed-login',
115+
conf: 'maximumLoginAttempts'
116+
},
113117
].forEach(function (configField) {
114118
console.log(configField);
115119
$(configField.elem).keyup(function (e) {

lib/AppInfo/Application.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,11 @@
2727
use OCA\Password_Policy\Capabilities;
2828
use OCA\Password_Policy\ComplianceService;
2929
use OCA\Password_Policy\Generator;
30+
use OCA\Password_Policy\Listener\FailedLoginListener;
31+
use OCA\Password_Policy\Listener\SuccesfullLoginListener;
3032
use OCA\Password_Policy\PasswordValidator;
3133
use OCP\AppFramework\App;
34+
use OCP\Authentication\Events\LoginFailedEvent;
3235
use OCP\EventDispatcher\Event;
3336
use OCP\EventDispatcher\IEventDispatcher;
3437
use OCP\ILogger;
@@ -37,6 +40,7 @@
3740
use OCP\User\Events\BeforePasswordUpdatedEvent;
3841
use OCP\User\Events\BeforeUserLoggedInEvent;
3942
use OCP\User\Events\PasswordUpdatedEvent;
43+
use OCP\User\Events\UserLoggedInEvent;
4044
use Symfony\Component\EventDispatcher\GenericEvent;
4145

4246
class Application extends App {
@@ -109,6 +113,9 @@ function (Event $event) use ($container) {
109113
}
110114
);
111115

116+
$eventDispatcher->addServiceListener(LoginFailedEvent::class, FailedLoginListener::class);
117+
$eventDispatcher->addServiceListener(UserLoggedInEvent::class, SuccesfullLoginListener::class);
118+
112119
// TODO: remove these two legacy event listeners
113120
$symfonyDispatcher = $server->getEventDispatcher();
114121
$symfonyDispatcher->addListener(

lib/FailedLoginCompliance.php

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php
2+
declare(strict_types=1);
3+
/**
4+
* @copyright Copyright (c) 2020, Roeland Jago Douma <roeland@famdouma.nl>
5+
*
6+
* @author Roeland Jago Douma <roeland@famdouma.nl>
7+
*
8+
* @license GNU AGPL version 3 or any later version
9+
*
10+
* This program is free software: you can redistribute it and/or modify
11+
* it under the terms of the GNU Affero General Public License as
12+
* published by the Free Software Foundation, either version 3 of the
13+
* License, or (at your option) any later version.
14+
*
15+
* This program is distributed in the hope that it will be useful,
16+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
17+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+
* GNU Affero General Public License for more details.
19+
*
20+
* You should have received a copy of the GNU Affero General Public License
21+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
22+
*
23+
*/
24+
25+
namespace OCA\Password_Policy;
26+
27+
use OCP\IConfig;
28+
use OCP\IUser;
29+
use OCP\IUserManager;
30+
31+
class FailedLoginCompliance {
32+
33+
/** @var IConfig */
34+
private $config;
35+
36+
/** @var IUserManager */
37+
private $userManager;
38+
39+
/** @var PasswordPolicyConfig */
40+
private $passwordPolicyConfig;
41+
42+
public function __construct(
43+
IConfig $config,
44+
IUserManager $userManager,
45+
PasswordPolicyConfig $passwordPolicyConfig) {
46+
$this->config = $config;
47+
$this->userManager = $userManager;
48+
$this->passwordPolicyConfig = $passwordPolicyConfig;
49+
}
50+
51+
public function onFailedLogin(string $uid) {
52+
$user = $this->userManager->get($uid);
53+
54+
if (!($user instanceof IUser)) {
55+
return;
56+
}
57+
58+
if ($user->isEnabled() === false) {
59+
// Just ignore this user then
60+
return;
61+
}
62+
63+
$allowedAttempts = $this->passwordPolicyConfig->getMaximumLoginAttempts();
64+
65+
$attempts = $this->getAttempts($uid);
66+
$attempts++;
67+
68+
if ($attempts >= $allowedAttempts) {
69+
$this->setAttempts($uid, 0);
70+
$user->setEnabled(false);
71+
return;
72+
}
73+
74+
$this->setAttempts($uid, $attempts);
75+
}
76+
77+
public function onSucessfullLogin(IUser $user) {
78+
$this->setAttempts($user->getUID(), 0);
79+
}
80+
81+
private function getAttempts(string $uid): int {
82+
return (int)$this->config->getUserValue($uid, 'password_policy', 'failedLoginAttempts', 0);
83+
}
84+
85+
private function setAttempts(string $uid, int $attempts): void {
86+
return $this->config->setUserValue($uid, 'password_policy', 'failedLoginAttempts', $attempts);
87+
}
88+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
declare(strict_types=1);
3+
/**
4+
* @copyright Copyright (c) 2020, Roeland Jago Douma <roeland@famdouma.nl>
5+
*
6+
* @author Roeland Jago Douma <roeland@famdouma.nl>
7+
*
8+
* @license GNU AGPL version 3 or any later version
9+
*
10+
* This program is free software: you can redistribute it and/or modify
11+
* it under the terms of the GNU Affero General Public License as
12+
* published by the Free Software Foundation, either version 3 of the
13+
* License, or (at your option) any later version.
14+
*
15+
* This program is distributed in the hope that it will be useful,
16+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
17+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+
* GNU Affero General Public License for more details.
19+
*
20+
* You should have received a copy of the GNU Affero General Public License
21+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
22+
*
23+
*/
24+
25+
namespace OCA\Password_Policy\Listener;
26+
27+
use OCA\Password_Policy\FailedLoginCompliance;
28+
use OCP\Authentication\Events\LoginFailedEvent;
29+
use OCP\EventDispatcher\Event;
30+
use OCP\EventDispatcher\IEventListener;
31+
32+
class FailedLoginListener implements IEventListener {
33+
34+
/** @var FailedLoginCompliance */
35+
private $compliance;
36+
37+
public function __construct(FailedLoginCompliance $compliance) {
38+
$this->compliance = $compliance;
39+
}
40+
41+
public function handle(Event $event): void {
42+
if (!($event instanceof LoginFailedEvent)) {
43+
return;
44+
}
45+
46+
$this->compliance->onFailedLogin($event->getUid());
47+
}
48+
49+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
declare(strict_types=1);
3+
/**
4+
* @copyright Copyright (c) 2020, Roeland Jago Douma <roeland@famdouma.nl>
5+
*
6+
* @author Roeland Jago Douma <roeland@famdouma.nl>
7+
*
8+
* @license GNU AGPL version 3 or any later version
9+
*
10+
* This program is free software: you can redistribute it and/or modify
11+
* it under the terms of the GNU Affero General Public License as
12+
* published by the Free Software Foundation, either version 3 of the
13+
* License, or (at your option) any later version.
14+
*
15+
* This program is distributed in the hope that it will be useful,
16+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
17+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+
* GNU Affero General Public License for more details.
19+
*
20+
* You should have received a copy of the GNU Affero General Public License
21+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
22+
*
23+
*/
24+
25+
namespace OCA\Password_Policy\Listener;
26+
27+
use OCA\Password_Policy\FailedLoginCompliance;
28+
use OCP\EventDispatcher\Event;
29+
use OCP\EventDispatcher\IEventListener;
30+
use OCP\User\Events\UserLoggedInEvent;
31+
32+
class SuccesfullLoginListener implements IEventListener {
33+
/** @var FailedLoginCompliance */
34+
private $compliance;
35+
36+
public function __construct(FailedLoginCompliance $compliance) {
37+
$this->compliance = $compliance;
38+
}
39+
40+
public function handle(Event $event): void {
41+
if (!($event instanceof UserLoggedInEvent)) {
42+
return;
43+
}
44+
45+
$this->compliance->onSucessfullLogin($event->getUser());
46+
}
47+
48+
}

lib/PasswordPolicyConfig.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,4 +193,14 @@ public function getExpiryInDays(): int {
193193
);
194194
}
195195

196+
/**
197+
* @return int if 0 then there is no limit
198+
*/
199+
public function getMaximumLoginAttempts(): int {
200+
return (int)$this->config->getAppValue(
201+
'password_policy',
202+
'maximumLoginAttempts',
203+
0
204+
);
205+
}
196206
}

lib/Settings.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public function getForm(): TemplateResponse {
4747
'enforceHaveIBeenPwned' => $this->config->getEnforceHaveIBeenPwned(),
4848
'historySize' => $this->config->getHistorySize(),
4949
'expiration' => $this->config->getExpiryInDays(),
50+
'maximumLoginAttempts' => $this->config->getMaximumLoginAttempts(),
5051
]);
5152

5253
return $response;

templates/settings-admin.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@
4747
<span><?php p($l->t('days until user password expires')) ?></span>
4848
</label>
4949
</p>
50+
<p>
51+
<label class="password-policy-number-option">
52+
<input id="password-policy-failed-login" type="number" value="<?php p($_['maximumLoginAttempts']) ?>" />
53+
<span><?php p($l->t('login attempts before the user account is blocked. (0 for no limit)')) ?></span>
54+
</label>
55+
</p>
5056
<p id="enforceNonCommonPassword">
5157
<input type="checkbox" name="password-policy-enforce-non-common-password" id="password-policy-enforce-non-common-password" class="checkbox"
5258
value="1" <?php if ($_['enforceNonCommonPassword']) print_unescaped('checked="checked"'); ?> />

0 commit comments

Comments
 (0)