KemalIdentity::ExponentialBackoffRateLimiter
Inherits KemalIdentity::RateLimiter < Reference < Object
An escalating delay between attempts, in memory.
Where FixedWindowRateLimiter allows N attempts and then nothing, this allows attempts
further and further apart: after the n-th consecutive failure the next attempt must wait
factor × 2^(n-1). With the default factor of one second that is 1, 2, 4, 8, 16 … seconds,
capped at max_delay.
This is django-otp's curve, and its ThrottlingMixin is the reference — there the pair
(throttling_failure_count, throttling_failure_timestamp) lives on the device row and
the delay is throttle_factor × 2^(n-1). Keycloak's brute-force detector is the same shape
under different names: a wait incremented per failure, bounded by a maximum wait.
Why the shape is worth having
A flat window is the same for an honest user and an attacker. Somebody who fat-fingers a
code twice waits a second and never notices; a machine guessing six digits is at hours per
attempt within a dozen tries, while a fixed window of twelve per five minutes hands out
103,680 attempts a month indefinitely — measured, in blueprints/0025 (MFA-04).
NIST SP 800-63B lists exactly this as a mitigation for the lockout its rate-limiting requirement would otherwise cause: "Requiring the claimant to wait after a failed attempt for a period of time that increases as the subscriber account approaches its maximum allowance for consecutive failed attempts (e.g., 30 seconds up to an hour)".
What it does not do
It is not the lifetime bound. The delay grows without ever refusing outright, so this
alone does not satisfy the SHALL that a verifier disable an authenticator after 100
consecutive failures. MFA::Service#max_consecutive_failures is that, and the two are
meant to be used together: this one makes guessing slow, that one makes it stop.
It is per process, like its sibling, for the same reason and with the same answer — a shared store behind this contract.
reset is what makes it "consecutive"
A success calls reset and the curve starts over. Nothing decays with time: an account
that failed eight times last year is still at eight until something succeeds. That is the
deliberate reading of "consecutive", and the reason MFA::Service clears a factor's
counter on success rather than on a timer.
Constants
One second, so the first repeat is barely felt and the tenth is over eight minutes.
An hour, which is the upper end of the example NIST gives.
Bounds memory, exactly as FixedWindowRateLimiter::DEFAULT_MAX_KEYS does.
Constructors
Instance methods
Counts one attempt against key and says whether it may proceed.
Called before any I/O and before any hashing. A denial must be cheap, or the limiter becomes the very lever it exists to remove.
Must not raise for a storage failure. A limiter whose Redis is unreachable returns
Verdict.unavailable and lets the application's configured policy decide, because the
answer differs per endpoint: a login should refuse rather than run unmetered, while a
less sensitive action may prefer to stay up. An exception here would make that choice for
everybody, and would surface as a 500 rather than as either policy.
How long the caller must wait before its next attempt, given count consecutive ones.
Clears the count for key, after a successful authentication.
Idempotent, and safe for a key that was never consumed. Must not raise, including when the store is unavailable: a reset that does not happen leaves somebody throttled slightly longer than they earned, which is not worth failing a successful login over.