Definition
- Rate limiting – Cap attempts per time window per account and per IP, enforced at the API gateway.
- Account lockout – Temporarily disable an account after too many failed logins; use progressive delays to avoid enabling DoS.
- CAPTCHA – A human-verification challenge shown after a few failures, blunting automated scripts.
- Multi-factor authentication (MFA) – Require a second proof (OTP, hardware key) so a password alone is useless.
- IP blacklisting – Block known-abusive sources, statically from threat intelligence or dynamically on threshold breach.
- Monitoring – Track failed-login rates and alert on spikes to catch credential stuffing across many accounts.
Imagine thieves trying thousands of PINs at a National Bank of India ATM. Rate limiting lets the machine accept only a few tries per minute. Account lockout swallows the card after three wrong PINs. CAPTCHA asks a question only a human can answer. MFA requires both the card and a one-time code sent to Rajesh's phone. IP blacklisting bans a specific thief who keeps coming back. Monitoring is the security camera that calls the guard when it sees a pattern.
Understanding the Concepts
Why is defence in depth necessary?
Each control covers a gap another leaves open. An attacker must defeat all of them at once.
package com.bank.security;
public class BruteForceDefenceInDepth {
public static void main(String[] args) {
System.out.println("Each layer catches a different attack:");
System.out.println(" Rate limit: caps velocity of attempts");
System.out.println(" Lockout: stops single-account guessing");
System.out.println(" CAPTCHA: blocks automated scripts");
System.out.println(" MFA: makes password alone useless");
System.out.println(" IP blacklist: blocks persistent source");
System.out.println(" Monitoring: catches credential stuffing across many accounts");
System.out.println(" Short tokens: limits damage if credentials compromised");
System.out.println();
System.out.println("Removing any one layer opens a specific hole.");
}
}
Expected output:
Each layer catches a different attack:
Rate limit: caps velocity of attempts
Lockout: stops single-account guessing
CAPTCHA: blocks automated scripts
MFA: makes password alone useless
IP blacklist: blocks persistent source
Monitoring: catches credential stuffing across many accounts
Short tokens: limits damage if credentials compromised
Removing any one layer opens a specific hole.
How does rate limiting work?
Cap the number of login attempts per time window per account and per IP, enforced at the API gateway with Redis.
package com.bank.security;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.time.Duration;
@Service
public class LoginRateLimiter {
private final StringRedisTemplate redis;
private static final int MAX_ATTEMPTS = 5;
private static final Duration WINDOW = Duration.ofMinutes(1);
public LoginRateLimiter(StringRedisTemplate redis) {
this.redis = redis;
}
public boolean isAllowed(String accountId, String ipAddress) {
String accountKey = "login:account:" + accountId;
String ipKey = "login:ip:" + ipAddress;
Long accountAttempts = redis.opsForValue().increment(accountKey);
Long ipAttempts = redis.opsForValue().increment(ipKey);
if (accountAttempts == 1) redis.expire(accountKey, WINDOW);
if (ipAttempts == 1) redis.expire(ipKey, WINDOW);
boolean allowed = accountAttempts <= MAX_ATTEMPTS && ipAttempts <= 20;
if (!allowed) {
System.out.println("Rate limited: account=" + accountAttempts
+ ", ip=" + ipAttempts);
}
return allowed;
}
}
Rate limit configuration:
Per account: 5 attempts per minute
Per IP: 20 attempts per minute (shared IPs like corporate NAT)
Response: 429 Too Many Requests
How does account lockout work without enabling DoS?
Naive lockout lets an attacker intentionally fail logins to lock out a legitimate user. Use progressive delays instead of hard permanent locks.
package com.bank.security;
public class ProgressiveLockout {
public static void main(String[] args) {
System.out.println("Progressive lockout (not hard lock):");
System.out.println(" Failure 1-3: immediate retry allowed");
System.out.println(" Failure 4: 30 second delay before next attempt");
System.out.println(" Failure 5: 5 minute delay");
System.out.println(" Failure 6+: 15 minute delay + notify account owner");
System.out.println();
System.out.println("WRONG: Permanent lock after 3 failures");
System.out.println(" Attacker locks out legitimate users intentionally (DoS)");
System.out.println();
System.out.println("CORRECT: Progressive delay + account-owner notification");
System.out.println(" Attacker is slowed; real user is notified and can reset");
}
}
Expected output:
Progressive lockout (not hard lock):
Failure 1-3: immediate retry allowed
Failure 4: 30 second delay before next attempt
Failure 5: 5 minute delay
Failure 6+: 15 minute delay + notify account owner
When do you show CAPTCHA?
After a few failed attempts, not on every login. This avoids annoying legitimate users while blocking bots.
package com.bank.security;
public class CaptchaStrategy {
public static void main(String[] args) {
System.out.println("CAPTCHA trigger strategy:");
System.out.println(" Attempt 1-2: no CAPTCHA (good UX for legitimate users)");
System.out.println(" Attempt 3+: show CAPTCHA (blocks automated scripts)");
System.out.println(" Successful login: reset CAPTCHA counter");
System.out.println();
System.out.println("Limitation: CAPTCHA solving services exist");
System.out.println(" -> CAPTCHA is one layer, not the whole defense");
}
}
Expected output:
CAPTCHA trigger strategy:
Attempt 1-2: no CAPTCHA (good UX for legitimate users)
Attempt 3+: show CAPTCHA (blocks automated scripts)
Successful login: reset CAPTCHA counter
Why is MFA the strongest single control?
Even if the attacker guesses the password, they need a second factor. For high-risk actions, require step-up MFA.
package com.bank.security;
public class MfaStrength {
public static void main(String[] args) {
System.out.println("MFA breaks the economics of brute force:");
System.out.println(" Without MFA: guess password -> full access");
System.out.println(" With MFA: guess password -> still blocked by OTP/hardware key");
System.out.println();
System.out.println("Step-up MFA for high-risk actions:");
System.out.println(" Login: password + OTP");
System.out.println(" View balance: JWT only (already authenticated)");
System.out.println(" Transfer > 100000: re-verify with OTP (step-up)");
}
}
Expected output:
MFA breaks the economics of brute force:
Without MFA: guess password -> full access
With MFA: guess password -> still blocked by OTP/hardware key
Step-up MFA for high-risk actions:
Login: password + OTP
View balance: JWT only (already authenticated)
Transfer > 100000: re-verify with OTP (step-up)
How does monitoring detect attacks?
Track failed-login rate and lockout counts. Alert when they spike. Detect credential stuffing that per-account lockout misses.
package com.bank.security;
public class AttackMonitoring {
public static void main(String[] args) {
System.out.println("Metrics to monitor:");
System.out.println(" login_failure_total (counter, per service)");
System.out.println(" login_lockout_total (counter, per service)");
System.out.println(" login_captcha_triggered_total (counter)");
System.out.println();
System.out.println("Alert rules:");
System.out.println(" login_failure_total > 1000/min -> credential stuffing attack");
System.out.println(" login_lockout_total > 100/min -> brute force on many accounts");
System.out.println(" Single IP with > 50 failures -> auto-blacklist IP");
}
}
Expected output:
Metrics to monitor:
login_failure_total (counter, per service)
login_lockout_total (counter, per service)
login_captcha_triggered_total (counter)
Alert rules:
login_failure_total > 1000/min -> credential stuffing attack
login_lockout_total > 100/min -> brute force on many accounts
Single IP with > 50 failures -> auto-blacklist IP
How do short-lived tokens limit damage?
Access tokens expire in minutes. A stolen credential has a tiny window of usefulness.
package com.bank.security;
public class ShortLivedTokens {
public static void main(String[] args) {
System.out.println("Token strategy:");
System.out.println(" Access token: 5 minutes (short blast radius)");
System.out.println(" Refresh token: 24 hours (rotated on each use)");
System.out.println(" Logout: revokes refresh token -> no new access tokens");
System.out.println(" Current access token expires in <= 5 minutes");
System.out.println();
System.out.println("Refresh token rotation:");
System.out.println(" Each use issues new refresh + access tokens");
System.out.println(" Reuse of old refresh token -> detect theft -> revoke all");
}
}
Expected output:
Token strategy:
Access token: 5 minutes (short blast radius)
Refresh token: 24 hours (rotated on each use)
Logout: revokes refresh token -> no new access tokens
Current access token expires in <= 5 minutes
What should a senior engineer know?
Hash passwords with BCrypt or Argon2, return generic failure messages, apply throttling to password-reset and OTP endpoints, and combine signals into a risk score.
Senior guidelines:
[x] BCrypt or Argon2 for password hashing (slow by design)
[x] Generic failure: 'Invalid credentials' (never reveal which was wrong)
[x] Throttle password-reset and OTP endpoints (common blind spots)
[x] Combine signals: unusual geo + new device + rising failures = step-up MFA
[x] Progressive delays, not hard permanent locks
[x] Apply same limits to API keys and service accounts
Visual Overview
Brute Force Defence Layers:
┌──────────────────────────────────────────────┐
│ Attacker: 10,000 credential-stuffing │
│ attempts per minute from botnet │
└──────────────────┬───────────────────────────┘
│
┌──────────────────▼───────────────────────────┐
│ Layer 1: Rate Limit (gateway) │
│ Per IP: 20/min, Per account: 5/min │
│ -> 95% of attempts blocked with 429 │
└──────────────────┬───────────────────────────┘
│ ~500 attempts pass
┌──────────────────▼───────────────────────────┐
│ Layer 2: Account Lockout + CAPTCHA │
│ After 3 failures: CAPTCHA required │
│ After 5 failures: 15-min progressive delay │
│ -> ~50 attempts pass CAPTCHA │
└──────────────────┬───────────────────────────┘
│ ~50 attempts with correct password
┌──────────────────▼───────────────────────────┐
│ Layer 3: MFA │
│ Correct password but no OTP -> BLOCKED │
│ -> 0 successful logins without 2nd factor │
└──────────────────────────────────────────────┘
Key Takeaway
Defending sensitive endpoints from brute force requires layers, because each control covers a different gap. Rate limiting caps the velocity of attempts per account and per IP. Account lockout with progressive delays stops single-account guessing without enabling DoS. CAPTCHA blocks automated scripts after a few failures. Multi-factor authentication is the strongest single control because a guessed password is useless without the second factor.
IP blacklisting blocks persistent abusive sources, monitoring detects credential stuffing across many accounts that per-account lockout misses, and short-lived tokens limit the blast radius of any eventual compromise. At National Bank of India, thousands of rapid attempts hit Rajesh's account. The gateway rate-limited and dynamically blacklisted the IP, the account locked after repeated failures with progressive delays, CAPTCHA was demanded after three, and even a correct password failed without the MFA code.
The senior nuances matter: hash passwords with BCrypt so offline guessing is expensive, return generic failure messages so you never reveal whether the username or password was wrong, apply the same throttling to password-reset and OTP endpoints which are common blind spots, and combine weak signals like unusual geography, new device, and rising failures into a risk score that triggers step-up authentication.
Remember one sentence: stop brute force with layered defence: rate-limit per account and IP, progressive lockout, CAPTCHA after failures, MFA so a password alone is worthless, monitoring for failure spikes, and short-lived tokens to minimize blast radius.
---