Rate Limiting Access Codes: The Delicate Balance Between Security and UX
When building DocBeacon, we faced a common but tricky engineering challenge: how to protect resources secured by short access codes without frustrating legitimate users.
The Strategic Choice: When to Increment?
The core of rate limiting lies in deciding which events trigger the counter.
- Strict Enforcement (Count Every Request): Every attempt, valid or not, consumes the limit. While secure, this often leads to "false positives"—locking out users due to flaky connections, page refreshes, or shared IP environments.
- Adaptive Enforcement (Count Only Failures): We opted for this approach. By only incrementing the counter on incorrect guesses, we ensure that a user with the correct code is never blocked by the security layer.
The Security Math
Is "counting only failures" risky? Let's look at the entropy. For a code of length L using a character set of size S, the total combinations C is:
C = S^L
If we allow n attempts per window, the probability P of a successful brute-force attack is:
P = n / C
By choosing an appropriate L and S, even with a generous n, the risk remains statistically negligible (often < 0.001%), making the UX gain far outweigh the marginal security risk.
Implementation Pattern: The Verification Flow
Instead of sharing our production code, here is the high-level logic pattern. Note how the rate limit is checked and incremented only after a failed validation:
// A generalized pattern for secure verification
async function verifyAccessWithLimit(inputCode, context) {
const { shareId, clientIp } = context;
const identifier = `limit:${shareId}:${clientIp}`;
// 1. Check if the user is already blocked
const isBlocked = await rateLimiter.isLimitReached(identifier);
if (isBlocked) {
throw new Error("Too many attempts. Please try again later.");
}
// 2. Validate the code
const isValid = await validateCode(inputCode);
if (isValid) {
return { success: true }; // Valid users are never penalized
}
// 3. Increment counter ONLY on failure
await rateLimiter.recordFailure(identifier);
return { success: false, error: "Invalid code" };
}
Key Design: Composite Identifiers
To prevent one attacker from affecting the entire system, we use composite keys. This ensures that rate limits are isolated:
// Abstracting the key generation to ensure isolation
function generateRateLimitKey(resourceId, userIp) {
// Combining resource and IP ensures that an attack on
// Resource A doesn't block legitimate access to Resource B.
return `ratelimit:res_${resourceId}:ip_${userIp}`;
}
Conclusion
True security is about proportional response. By focusing our defenses on failure patterns rather than total traffic, we created a system for DocBeacon that feels invisible to the user but remains an impenetrable wall for brute-force scripts.
Top comments (0)