Q12 of 24 · Security
How do you test rate limiting and brute-force protection on an authentication endpoint?
Short answer
Short answer: Send a rapid sequence of login requests with invalid credentials and assert the application responds with 429 Too Many Requests, introduces an account lockout, or requires a CAPTCHA after the configured threshold. Verify that a valid login after the lockout window succeeds.
Detail
Brute-force protection comes in several forms — test each specifically:
Rate limiting (429 response):
Send 20-30 requests in quick succession to the login endpoint with invalid credentials. Assert HTTP 429 is returned at or before the configured threshold. Check the Retry-After header — it should specify when the client can retry. Verify the rate limit resets correctly after the window expires.
Account lockout: Submit N failed login attempts for a specific username. Verify the account is locked (response changes to 403 with a "locked" message, or the next attempt fails even with correct credentials). Verify the lockout duration is reasonable (5-15 minutes) and that there is a legitimate unlock mechanism (email link, admin reset).
CAPTCHA / device verification: After N failures, some applications introduce a CAPTCHA challenge. Verify the challenge is enforced before the Nth+1 attempt — not after.
Edge cases to test:
- Rate limiting by IP vs by account: an attacker distributing requests across IPs should still be rate-limited per account.
- Timing attacks: does the response time differ noticeably between valid and invalid usernames? A significant timing difference can be used to enumerate valid accounts.
- "Slow" brute force: does rate limiting apply only to rapid bursts, or also to slow password spraying across hours?
What not to do in testing: use a test account dedicated to security testing — never run brute-force tests against production accounts, as lockouts will affect real users.
// EXAMPLE
rate-limit.test.ts
test('login endpoint rate-limits after 10 failed attempts', async ({ request }) => {
for (let i = 0; i < 10; i++) {
await request.post('/api/login', {
data: { email: 'test@example.com', password: 'wrong-password' },
});
}
const response = await request.post('/api/login', {
data: { email: 'test@example.com', password: 'wrong-password' },
});
expect(response.status()).toBe(429);
expect(response.headers()['retry-after']).toBeTruthy();
});