BookStackApp/BookStack · warning · NotifyException
auth.mfa_throttle
Error message
auth.mfa_throttle
What it means
When MFA verification is attempted too many times, MfaVerificationLimiter::throwException raises a NotifyException with the translated 'auth.mfa_throttle' message (including a 60-second wait) and HTTP 429, redirecting the user to /login. This is intentional rate limiting to slow brute-force MFA code guessing.
Source
Thrown at app/Access/Mfa/MfaVerificationLimiter.php:28
/**
* A rate limit specifically for MFA verification.
* Limits across both the attempted user (on a tight limit) and the
* request IP (on a less strict limit).
*/
class MfaVerificationLimiter
{
protected int $maxUserAttemptsPerMinute = 5;
protected int $maxIpAttemptsPerMinute = 60;
public function __construct(
protected RateLimiter $rateLimiter
) {
}
public function throwException(): never
{
throw new NotifyException(
trans('auth.mfa_throttle', ['seconds' => 60]),
'/login',
Response::HTTP_TOO_MANY_REQUESTS
);
}
public function incrementAttempts(User $user, Request $request): void
{
$this->rateLimiter->hit($this->getUserKey($user));
$this->rateLimiter->hit($this->getRequestKey($request));
}
public function decrementAttempts(User $user, Request $request): void
{
$this->rateLimiter->decrement($this->getUserKey($user));
$this->rateLimiter->decrement($this->getRequestKey($request));
}
View on GitHub (pinned to 18f8469a1c)
Solutions
- Wait 60 seconds (or the configured throttle window) before attempting MFA verification again
- Clear the rate limiter key (RateLimiter::clear) in admin/testing contexts to reset the counter
- Ask affected users to check they are entering the correct current TOTP/backup code to avoid repeat failures
- Increase the limiter threshold or window in code if it is too aggressive for your user base
- Ensure automated clients back off on HTTP 429 instead of retrying immediately
Example fix
// before (test/automation retry loop)
while (!$mfaService->verifyCode($user, $code)) { /* retry immediately */ }
// after
if ($attempts >= $maxAttempts) {
sleep(60); // respect the mfa throttle window
break;
}
$mfaService->verifyCode($user, $code); Defensive patterns
Strategy: retry
Validate before calling
// check remaining attempts before verifying
if (RateLimiter::tooManyAttempts($mfaKey, $maxAttempts)) {
$seconds = RateLimiter::availableIn($mfaKey);
return back()->withErrors("Try again in {$seconds} seconds");
} Try / catch
try {
$mfaService->verifyCode($user, $code);
} catch (NotifyException $e) {
if ($e->getStatusCode() === 429) {
return redirect($e->redirectTo())->withErrors($e->getMessage()); // show throttle message
}
throw $e;
} Prevention
- Clients must back off on HTTP 429 instead of retrying immediately
- Show a countdown UI to users after failed MFA attempts
- Clear rate limiter keys between test runs
- Ensure TOTP clock sync so users don't fail codes repeatedly
When it happens
Trigger: A user exceeds the rate limit for MFA code verification attempts within the limiter's window (RateLimiter hit limit), so throwException() fires on the next attempt instead of validating the code.
Common situations: User repeatedly typing wrong MFA codes; automated scripts or tests hammering the MFA endpoint; shared NAT/proxy where many users share one IP so the limiter trips collectively; clock/session issues causing retries in a loop.
Related errors
AI-assisted analysis of BookStackApp/BookStack@18f8469a1c (2026-09-02).
Data as JSON: /api/errors/f785789e79215c94.
Report an issue: GitHub.