Skip to content

Commit 7e4fc60

Browse files
authored
Merge pull request #17454 from craftcms/bugfix/17445-max-invalid-logins-passkeys-and-2fa
bugfix/17445 2fa, passkeys and `maxInvalidLogins`
2 parents 88af813 + e9d9fe2 commit 7e4fc60

File tree

4 files changed

+56
-13
lines changed

4 files changed

+56
-13
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Unreleased
44

55
- Added `craft\fields\linktypes\BaseLinkType::isValueEmpty()`.
6+
- Added `craft\services\Auth::getAuthErrorMessage()`.
67
- Added `craft\services\ElementSources::sourceExists()`.
78
- Added new icons. ([#17441](https://github.com/craftcms/cms/pull/17441))
89
- Updated yii2-debug to 2.1.27. ([#17115](https://github.com/craftcms/cms/issues/17115))
@@ -12,6 +13,7 @@
1213
- Fixed a bug where entries that were pasted within a Matrix field were losing custom field translations for other sites. ([17414](https://github.com/craftcms/cms/issues/17414))
1314
- Fixed a bug where Matrix fields’ chips were getting “Copy all entries” actions. ([#17404](https://github.com/craftcms/cms/issues/17404))
1415
- Fixed a bug where Link fields were validating if a deleted element was selected. ([#17409](https://github.com/craftcms/cms/issues/17409))
16+
- Users’ maximum invalid login counts now factor in invalid two-step verification attempts. ([#17454](https://github.com/craftcms/cms/pull/17454))
1517

1618
## 5.7.10 - 2025-06-04
1719

src/controllers/AuthController.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,10 @@ public function actionVerifyTotp(): Response
136136
$this->requireAcceptsJson();
137137

138138
$code = $this->request->getRequiredBodyParam('code');
139+
$authService = Craft::$app->getAuth();
139140

140-
if (!Craft::$app->getAuth()->verify(TOTP::class, $code)) {
141-
return $this->asFailure(Craft::t('app', 'Invalid verification code.'));
141+
if (!$authService->verify(TOTP::class, $code)) {
142+
return $this->asFailure($authService->getAuthErrorMessage());
142143
}
143144

144145
return $this->asSuccess(Craft::t('app', 'Verification successful.'));
@@ -155,9 +156,10 @@ public function actionVerifyRecoveryCode(): Response
155156
$this->requireAcceptsJson();
156157

157158
$code = $this->request->getRequiredBodyParam('code');
159+
$authService = Craft::$app->getAuth();
158160

159-
if (!Craft::$app->getAuth()->verify(RecoveryCodes::class, $code)) {
160-
return $this->asFailure(Craft::t('app', 'Invalid recovery code.'));
161+
if (!$authService->verify(RecoveryCodes::class, $code)) {
162+
return $this->asFailure($authService->getAuthErrorMessage(Craft::t('app', 'Invalid recovery code.')));
161163
}
162164

163165
return $this->asSuccess(Craft::t('app', 'Verification successful.'));

src/elements/User.php

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1333,6 +1333,23 @@ public function validateAuthKey($authKey): ?bool
13331333
return true;
13341334
}
13351335

1336+
/**
1337+
* Handles an invalid login for a user and sets the authError param.
1338+
*
1339+
* @return void
1340+
*/
1341+
public function handleInvalidLoginParam(): void
1342+
{
1343+
Craft::$app->getUsers()->handleInvalidLogin($this);
1344+
// Was that one bad password/2fa code/passkey too many?
1345+
if ($this->locked && !Craft::$app->getConfig()->getGeneral()->preventUserEnumeration) {
1346+
// Will set the authError to either AccountCooldown or AccountLocked
1347+
$this->authError = $this->_getAuthError();
1348+
} else {
1349+
$this->authError = self::AUTH_INVALID_CREDENTIALS;
1350+
}
1351+
}
1352+
13361353
/**
13371354
* Determines whether the user is allowed to be logged in with a given password.
13381355
*
@@ -1363,14 +1380,7 @@ public function authenticate(string $password): bool
13631380
}
13641381

13651382
if (!$passwordValid) {
1366-
Craft::$app->getUsers()->handleInvalidLogin($this);
1367-
// Was that one bad password too many?
1368-
if ($this->locked && !Craft::$app->getConfig()->getGeneral()->preventUserEnumeration) {
1369-
// Will set the authError to either AccountCooldown or AccountLocked
1370-
$this->authError = $this->_getAuthError();
1371-
} else {
1372-
$this->authError = self::AUTH_INVALID_CREDENTIALS;
1373-
}
1383+
$this->handleInvalidLoginParam();
13741384
return false;
13751385
}
13761386

@@ -1421,7 +1431,7 @@ public function authenticateWithPasskey(
14211431
}
14221432

14231433
if (!$keyValid) {
1424-
$this->authError = self::AUTH_INVALID_CREDENTIALS;
1434+
$this->handleInvalidLoginParam();
14251435
return false;
14261436
}
14271437

src/services/Auth.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use craft\helpers\DateTimeHelper;
2222
use craft\helpers\Json;
2323
use craft\helpers\Session as SessionHelper;
24+
use craft\helpers\User as UserHelper;
2425
use craft\models\UserGroup;
2526
use craft\records\WebAuthn as WebAuthnRecord;
2627
use craft\web\Session;
@@ -204,6 +205,7 @@ public function verify(string $methodClass, mixed ...$args): bool
204205
$user = $this->getUser($sessionDuration);
205206

206207
if (!$this->getMethod($methodClass, $user)->verify(...$args)) {
208+
$user?->handleInvalidLoginParam();
207209
return false;
208210
}
209211

@@ -224,6 +226,33 @@ public function verify(string $methodClass, mixed ...$args): bool
224226
return true;
225227
}
226228

229+
/**
230+
* Returns an authentication error message based on the authentication error value.
231+
* If a default message was passed and the authentication error value is for invalid credentials,
232+
* that default message will be used.
233+
*
234+
* @param string|null $defaultMessage
235+
* @return string
236+
* @since 5.7.11
237+
*/
238+
public function getAuthErrorMessage(?string $defaultMessage = null): string
239+
{
240+
$user = $this->getUser();
241+
$authError = null;
242+
if ($user) {
243+
$authError = UserHelper::getAuthStatus($user);
244+
}
245+
if ($authError == User::AUTH_INVALID_CREDENTIALS || !$authError) {
246+
if ($defaultMessage) {
247+
return $defaultMessage;
248+
}
249+
250+
return Craft::t('app', 'Invalid verification code.');
251+
}
252+
253+
return UserHelper::getLoginFailureMessage($authError, $user);
254+
}
255+
227256
/**
228257
* Returns all available user authentication methods.
229258
*

0 commit comments

Comments
 (0)