Skip to content

Commit f4ab6eb

Browse files
authored
feat: add login attempt counter (#82)
* feat: add login attempt counter Change-Id: Icd5ceb7f886ffa918449c872047ce4f279ee9c81 * fix: remove hard coded test value Change-Id: Ib765702819565ec82e1bc60361aee27aa492350b
1 parent 893d2f6 commit f4ab6eb

6 files changed

Lines changed: 76 additions & 16 deletions

File tree

app/Http/Controllers/UserController.php

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,7 @@ public function resendVerificationEmail(LaravelRequest $request)
393393
public function postLogin()
394394
{
395395
$max_login_attempts_2_show_captcha = $this->server_configuration_service->getConfigValue("MaxFailed.LoginAttempts.2ShowCaptcha");
396+
$max_login_failed_attempts = intval($this->server_configuration_service->getConfigValue("MaxFailed.Login.Attempts"));
396397
$login_attempts = 0;
397398
$username = '';
398399
$user = null;
@@ -479,13 +480,15 @@ public function postLogin()
479480
(
480481
[
481482
'max_login_attempts_2_show_captcha' => $max_login_attempts_2_show_captcha,
483+
'max_login_failed_attempts' => $max_login_failed_attempts,
482484
'login_attempts' => $login_attempts,
483485
'error_message' => $ex->getMessage(),
484486
'user_fullname' => !is_null($user) ? $user->getFullName() : "",
485487
'user_pic' => !is_null($user) ? $user->getPic(): "",
486488
'user_verified' => true,
487489
'username' => $username,
488-
'flow' => $flow
490+
'flow' => $flow,
491+
'user_is_active' => !is_null($user) ? ($user->isActive() ? 1 : 0) : 0
489492
]
490493
);
491494
}
@@ -495,6 +498,7 @@ public function postLogin()
495498
// validator errors
496499
$response_data = [
497500
'max_login_attempts_2_show_captcha' => $max_login_attempts_2_show_captcha,
501+
'max_login_failed_attempts' => $max_login_failed_attempts,
498502
'login_attempts' => $login_attempts,
499503
'validator' => $validator,
500504
];
@@ -506,7 +510,8 @@ public function postLogin()
506510
if(!is_null($user)){
507511
$response_data['user_fullname'] = $user->getFullName();
508512
$response_data['user_pic'] = $user->getPic();
509-
$response_data['user_verified'] = true;
513+
$response_data['user_verified'] = 1;
514+
$response_data['user_is_active'] = $user->isActive() ? 1 : 0;
510515
}
511516

512517
return $this->login_strategy->errorLogin
@@ -521,9 +526,10 @@ public function postLogin()
521526

522527
$response_data = [
523528
'max_login_attempts_2_show_captcha' => $max_login_attempts_2_show_captcha,
529+
'max_login_failed_attempts' => $max_login_failed_attempts,
524530
'login_attempts' => $login_attempts,
525531
'username' => $username,
526-
'error_message' => $ex1->getMessage()
532+
'error_message' => $ex1->getMessage(),
527533
];
528534

529535
if (is_null($user) && isset($data['username'])) {
@@ -533,7 +539,8 @@ public function postLogin()
533539
if(!is_null($user)){
534540
$response_data['user_fullname'] = $user->getFullName();
535541
$response_data['user_pic'] = $user->getPic();
536-
$response_data['user_verified'] = true;
542+
$response_data['user_verified'] = 1;
543+
$response_data['user_is_active'] = $user->isActive() ? 1 : 0;
537544
}
538545

539546
return $this->login_strategy->errorLogin

app/Services/SecurityPolicies/LockUserCounterMeasure.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public function trigger(array $params = [])
7777
$user_id = $params["user_id"];
7878
$user = $this->repository->getById($user_id);
7979
$max_login_failed_attempts = intval($this->server_configuration->getConfigValue("MaxFailed.Login.Attempts"));
80-
if (!is_null($user) && $user instanceof User) {
80+
if ($user instanceof User) {
8181
//apply lock policy
8282
if (intval($user->getLoginFailedAttempt()) < $max_login_failed_attempts) {
8383
$this->user_service->updateFailedLoginAttempts($user->getId());

app/libs/Auth/Factories/UserFactory.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
* See the License for the specific language governing permissions and
1212
* limitations under the License.
1313
**/
14-
use Auth\Group;
1514
use Auth\User;
1615
use Illuminate\Support\Facades\Auth;
1716

app/libs/Auth/Models/User.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1854,6 +1854,8 @@ public function activate():void {
18541854
if(!$this->active) {
18551855
$this->active = true;
18561856
$this->spam_type = self::SpamTypeHam;
1857+
// reset it
1858+
$this->login_failed_attempt = 0;
18571859
Event::dispatch(new UserSpamStateUpdated(
18581860
$this->getId()
18591861
)
@@ -1886,6 +1888,7 @@ public function verifyEmail(bool $send_email_verified_notice = true)
18861888
$this->spam_type = self::SpamTypeHam;
18871889
$this->active = true;
18881890
$this->lock = false;
1891+
$this->login_failed_attempt = 0;
18891892
$this->email_verified_date = new \DateTime('now', new \DateTimeZone('UTC'));
18901893

18911894
if($send_email_verified_notice)

resources/js/login/login.js

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,10 @@ const PasswordInputForm = ({
8282
captchaPublicKey,
8383
onChangeRecaptcha,
8484
handleEmitOtpAction,
85-
forgotPasswordAction
85+
forgotPasswordAction,
86+
loginAttempts,
87+
maxLoginFailedAttempts,
88+
userIsActive
8689
}) => {
8790
return (
8891
<form method="post" action={formAction} onSubmit={onAuthenticate} target="_self">
@@ -114,9 +117,46 @@ const PasswordInputForm = ({
114117
)
115118
}}
116119
/>
117-
{passwordError &&
118-
<p className={styles.error_label} dangerouslySetInnerHTML={{__html: passwordError}}></p>
119-
}
120+
{(() => {
121+
const attempts = parseInt(loginAttempts, 10);
122+
const maxAttempts = parseInt(maxLoginFailedAttempts, 10);
123+
const attemptsLeft = maxAttempts - attempts;
124+
125+
if (!passwordError) return null;
126+
127+
if (attempts > 0 && attempts < maxAttempts && userIsActive) {
128+
return (
129+
<>
130+
<p className={styles.error_label}>
131+
Incorrect password. You have {attemptsLeft} more attempt{attemptsLeft !== 1 ? 's' : ''} before your account is locked.
132+
</p>
133+
</>
134+
);
135+
}
136+
137+
if (attempts > 0 && attempts === maxAttempts && userIsActive) {
138+
return (
139+
<>
140+
<p className={styles.error_label}>
141+
Incorrect password. You have reached the maximum ({maxAttempts}) login attempts. Your account will be locked after another failed login.
142+
</p>
143+
</>
144+
);
145+
}
146+
147+
if (attempts > 0 && attempts === maxAttempts && !userIsActive) {
148+
return (
149+
<>
150+
<p className={styles.error_label}>
151+
Your account has been locked due to multiple failed login attempts. Please contact support to unlock it.
152+
</p>
153+
</>
154+
);
155+
}
156+
157+
return <p className={styles.error_label} dangerouslySetInnerHTML={{__html: passwordError}}></p>;
158+
})()}
159+
120160
<Grid container spacing={1}>
121161
<Grid item xs={12}>
122162
<Button variant="contained"
@@ -434,7 +474,7 @@ class LoginPage extends React.Component {
434474
this.resendVerificationEmail = this.resendVerificationEmail.bind(this);
435475
this.handleSnackbarClose = this.handleSnackbarClose.bind(this);
436476
this.showAlert = this.showAlert.bind(this);
437-
}
477+
}
438478

439479
showAlert(message, severity) {
440480
this.setState({
@@ -777,6 +817,9 @@ class LoginPage extends React.Component {
777817
onChangeRecaptcha={this.onChangeRecaptcha}
778818
handleEmitOtpAction={this.handleEmitOtpAction}
779819
forgotPasswordAction={this.props.forgotPasswordAction}
820+
loginAttempts={this.props?.loginAttempts}
821+
maxLoginFailedAttempts={this.props?.maxLoginFailedAttempts}
822+
userIsActive={this.props?.user_is_active}
780823
/>
781824
<HelpLinks
782825
userName={this.state.user_name}

resources/views/auth/login.blade.php

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,27 +58,35 @@
5858
config.maxLoginAttempts2ShowCaptcha = {{Session::get("max_login_attempts_2_show_captcha")}};
5959
@endif
6060
61+
@if(Session::has('max_login_failed_attempts'))
62+
config.maxLoginFailedAttempts = {{Session::get("max_login_failed_attempts")}};
63+
@endif
64+
6165
@if(Session::has('login_attempts'))
6266
config.loginAttempts = {{Session::get("login_attempts")}};
6367
@endif
6468
69+
@if(Session::has('user_is_active'))
70+
config.user_is_active = {{Session::get("user_is_active")}};
71+
@endif
72+
6573
@if(Session::has('user_fullname'))
6674
config.user_fullname = '{{Session::get("user_fullname")}}';
6775
@endif
6876
6977
@if(Session::has('user_pic'))
7078
config.user_pic = '{{Session::get("user_pic")}}';
7179
@endif
72-
@if(Session::has('user_verified'))
80+
@if(Session::has('user_verified'))
7381
config.user_verified = {{Session::get('user_verified')}};
7482
@endif
75-
@if(Session::has('flow'))
83+
@if(Session::has('flow'))
7684
config.flow = '{{Session::get('flow')}}';
7785
@endif
7886
79-
window.VERIFY_ACCOUNT_ENDPOINT = config.accountVerifyAction;
80-
window.EMIT_OTP_ENDPOINT = config.emitOtpAction;
81-
window.RESEND_VERIFICATION_EMAIL_ENDPOINT = config.resendVerificationEmailAction;
87+
window.VERIFY_ACCOUNT_ENDPOINT = config.accountVerifyAction;
88+
window.EMIT_OTP_ENDPOINT = config.emitOtpAction;
89+
window.RESEND_VERIFICATION_EMAIL_ENDPOINT = config.resendVerificationEmailAction;
8290
</script>
8391
{!! script_to('assets/login.js') !!}
8492
@append

0 commit comments

Comments
 (0)