Skip to content

Commit 3bde242

Browse files
committed
Merge branch 'b-7.4.x-attempts-limit-OXDEV-10036' into b-7.4.x-2fa-OXDEV-9078
2 parents f9ce596 + 6262770 commit 3bde242

16 files changed

Lines changed: 307 additions & 34 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.btn-responsive {
2+
width: 100%;
3+
}
4+
5+
@media (min-width: 768px) {
6+
.btn-responsive {
7+
width: auto;
8+
}
9+
}

assets/out/src/js/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ if (resendBtn) {
5353
const resendOtp = new ResendOtp(resendBtn);
5454

5555
// restore cooldown on refresh
56-
resendOtp.restoreIfNeeded();
56+
resendOtp.restoreOnRefresh();
5757
resendBtn.addEventListener('click', function () {
5858
resendOtp.resend();
5959
});

assets/out/src/js/module/resend-otp.js

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,38 @@
44
*/
55

66
export class ResendOtp {
7-
constructor(button, { hintId = 'resend-hint' } = {}) {
7+
constructor(button, options = {}) {
8+
const {
9+
hintId = 'resend-hint',
10+
submitButtonId = 'auth_submit',
11+
attemptsDisplayId = 'remaining-attempts',
12+
codeInputId = 'auth_code',
13+
maxAttempts = 5
14+
} = options;
15+
816
this.btn = button;
917
this.hint = document.getElementById(hintId);
18+
this.submitBtn = document.getElementById(submitButtonId);
19+
this.attemptsDisplay = document.getElementById(attemptsDisplayId);
20+
this.codeInput = document.getElementById(codeInputId);
21+
this.maxAttempts = maxAttempts;
1022

1123
this.cooldownSeconds = Number(button.dataset.cooldown || 60);
1224
this.url = button.dataset.url;
1325
this.storageKey = `otp_resend_until_${this.url}`;
1426

1527
this.timer = null;
28+
29+
this.initAttemptsCheck();
30+
}
31+
32+
initAttemptsCheck() {
33+
if (this.attemptsDisplay) {
34+
const attempts = parseInt(this.attemptsDisplay.textContent, 10);
35+
if (attempts === 0) {
36+
this.disableSubmit();
37+
}
38+
}
1639
}
1740

1841
async resend() {
@@ -31,13 +54,40 @@ export class ResendOtp {
3154
this.storeUntil(until);
3255
this.startCooldown(until);
3356

57+
this.resetAttempts();
58+
3459
} catch (e) {
3560
console.error(e);
3661
this.unlock();
3762
this.setHint('Could not resend code.');
3863
}
3964
}
4065

66+
resetAttempts() {
67+
if (this.attemptsDisplay) {
68+
this.attemptsDisplay.textContent = this.maxAttempts;
69+
}
70+
this.enableSubmit();
71+
}
72+
73+
disableSubmit() {
74+
if (this.submitBtn) {
75+
this.submitBtn.disabled = true;
76+
}
77+
if (this.codeInput) {
78+
this.codeInput.disabled = true;
79+
}
80+
}
81+
82+
enableSubmit() {
83+
if (this.submitBtn) {
84+
this.submitBtn.disabled = false;
85+
}
86+
if (this.codeInput) {
87+
this.codeInput.disabled = false;
88+
}
89+
}
90+
4191
startCooldown(until) {
4292
clearInterval(this.timer);
4393
console.log(`Starting OTP resend cooldown until ${new Date(until).toISOString()}`);
@@ -83,7 +133,7 @@ export class ResendOtp {
83133
localStorage.removeItem(this.storageKey);
84134
}
85135

86-
restoreIfNeeded() {
136+
restoreOnRefresh() {
87137
console.log('Restoring OTP resend cooldown if needed');
88138
const until = this.getStoredUntil();
89139
if (until && until > Date.now()) {

src/Authentication/TwoFactorAuth/Service/AuthorizeService.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,17 @@ public function getVerificationUrl(): string
7272

7373
return $verificator->getVerificationUrl();
7474
}
75+
76+
public function getRemainingAttempts(): int
77+
{
78+
$activeVerificator = $this->moduleSettings->getTwoFactorAuthType();
79+
80+
$verificator = $this->verifyCollector->getVerificator(
81+
$activeVerificator
82+
);
83+
84+
$userId = $this->session->get(self::USER_SESSION_KEY);
85+
86+
return $verificator->getRemainingAttempts($userId);
87+
}
7588
}

src/Authentication/TwoFactorAuth/Service/AuthorizeServiceInterface.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,6 @@ public function validate(string $inputCode): void;
1414
public function generate(): void;
1515

1616
public function getVerificationUrl(): string;
17+
18+
public function getRemainingAttempts(): int;
1719
}

src/Authentication/TwoFactorAuth/Service/Verificator/OTP/OTPVerificator.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
namespace OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Service\Verificator\OTP;
1111

1212
use OxidEsales\Eshop\Core\Config;
13+
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Exception\AttemptLimitExceededException;
1314
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Exception\InvalidCodeException;
1415
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Infrastructure\Repository\UserRepositoryInterface;
1516
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Service\Verificator\OTP\Generator\OTPGeneratorInterface;
@@ -42,6 +43,10 @@ public function validateCode(string $userId, string $inputCode): void
4243
$this->otpValidator->validateCode($otpData->getCode(), $inputCode);
4344
} catch (InvalidCodeException $e) {
4445
$this->userRepository->updateAttempts($userId, $otpData->getAttempts() + 1);
46+
if ($this->getRemainingAttempts($userId) === 0) {
47+
throw new AttemptLimitExceededException();
48+
}
49+
4550
throw $e;
4651
}
4752

@@ -57,4 +62,12 @@ public function getVerificationUrl(): string
5762
{
5863
return $this->config->getShopHomeUrl() . 'cl=twofactorauth';
5964
}
65+
66+
public function getRemainingAttempts(string $userId): int
67+
{
68+
$otpData = $this->userRepository->getUserOTPData($userId);
69+
$maxAttempts = $this->otpValidator->getMaxAttempts();
70+
71+
return max(0, $maxAttempts - $otpData->getAttempts());
72+
}
6073
}

src/Authentication/TwoFactorAuth/Service/Verificator/OTP/Validator/OTPValidator.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,9 @@ public function checkExpirationTime(?DateTimeInterface $expiresAt): void
4545
throw new TimeExpiredException();
4646
}
4747
}
48+
49+
public function getMaxAttempts(): int
50+
{
51+
return self::MAX_ATTEMPTS;
52+
}
4853
}

src/Authentication/TwoFactorAuth/Service/Verificator/OTP/Validator/OTPValidatorInterface.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,6 @@ public function validateCode(?string $userCode, string $inputCode): void;
2020
public function checkLoginAttempts(int $attempts): void;
2121

2222
public function checkExpirationTime(?DateTimeInterface $expiresAt): void;
23+
24+
public function getMaxAttempts(): int;
2325
}

src/Authentication/TwoFactorAuth/Service/Verificator/VerificatorAdapterInterface.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,6 @@ public function validateCode(string $userId, string $inputCode): void;
1616
public function generate(string $userId): string;
1717

1818
public function getVerificationUrl(): string;
19+
20+
public function getRemainingAttempts(string $userId): int;
1921
}

src/Shared/Core/ViewConfig.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
namespace OxidEsales\SecurityModule\Shared\Core;
1111

1212
use OxidEsales\SecurityModule\Authentication\OAuth2\Service\ProviderCollectorInterface;
13+
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Service\AuthorizeServiceInterface;
1314
use OxidEsales\SecurityModule\Captcha\Captcha\Image\Service\ImageCaptchaService;
1415
use OxidEsales\SecurityModule\Captcha\Service\CaptchaServiceInterface;
1516
use OxidEsales\SecurityModule\PasswordPolicy\Service\ModuleSettingsServiceInterface as PasswordSettingsServiceInterface;
@@ -58,4 +59,9 @@ public function getActiveProviders(): iterable
5859

5960
return array_filter($providers, fn($provider) => $provider->isActive());
6061
}
62+
63+
public function getRemainingAttempts(): int
64+
{
65+
return $this->getService(AuthorizeServiceInterface::class)->getRemainingAttempts();
66+
}
6167
}

0 commit comments

Comments
 (0)