Apply rate limiting to every authentication endpoint: login, password reset, OTP verification, account recovery, and token refresh. Limit by IP address and by account. Implement exponential backoff after failed attempts.
For GraphQL: count operations inside a batch, not just HTTP requests. A batch of 100 login mutations counts as 100 attempts, not 1.
Add CAPTCHA or proof-of-work on repeated failures. Lock accounts temporarily after a threshold of failed attempts, and notify the account owner.
Think of it like a bank vault with a 3-wrong-attempts-and-alarm-triggers rule. Every door into the vault — every auth endpoint — needs the same rule. Leaving one door unguarded means the others don't matter.
Rate limiting by IP is bypassable by distributed attacks (botnets rotate IPs). Credential stuffing with correct passwords from leaked lists won't trigger lockouts because each attempt succeeds on the first try. Rate limiting slows attackers; it doesn't stop credential stuffing. MFA does.
Ask whether there is a limit on how long a logged-in session stays active. A token that works forever after a single login is a permanent credential for anyone who obtains it.