feat: add rate limiting middleware for authentication endpoints

Add slowapi-based rate limiting: 5/min on password login, 10/min on
WebAuthn login. Includes shared rate limiter reset fixture for tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Johan Lundberg 2026-03-31 15:23:51 +02:00
parent 23ca6272a2
commit d4acb46cf5
No known key found for this signature in database
GPG key ID: A6C152738D03C7D1
5 changed files with 52 additions and 0 deletions

View file

@ -6,6 +6,7 @@ from httpx import ASGITransport, AsyncClient
from porchlight.app import create_app
from porchlight.config import Settings
from porchlight.rate_limit import limiter
@pytest.fixture
@ -21,6 +22,12 @@ async def client(settings: Settings) -> AsyncIterator[AsyncClient]:
yield ac
@pytest.fixture(autouse=True)
def _reset_rate_limiter() -> None:
"""Reset the rate limiter storage before each test."""
limiter.reset()
async def get_csrf_token(client: AsyncClient) -> str:
"""Get a CSRF token by visiting the login page.

25
tests/test_rate_limit.py Normal file
View file

@ -0,0 +1,25 @@
import pytest
from httpx import AsyncClient
from tests.conftest import get_csrf_token
@pytest.mark.asyncio
async def test_password_login_rate_limited(client: AsyncClient) -> None:
"""After 5 failed attempts, the 6th should be rate-limited."""
token = await get_csrf_token(client)
for _ in range(5):
await client.post(
"/login/password",
data={"username": "nobody", "password": "wrong"},
headers={"X-CSRF-Token": token},
)
response = await client.post(
"/login/password",
data={"username": "nobody", "password": "wrong"},
headers={"X-CSRF-Token": token},
)
assert response.status_code == 429