Add rate limiting to login endpoint

Security:
- Add slowapi dependency for rate limiting
- Create rate_limit.py module with configurable limits
- Apply 5 requests/minute limit to login endpoint
- Make rate limit configurable via ORCHARD_LOGIN_RATE_LIMIT env var

Testing:
- Set high rate limit (1000/min) in docker-compose.local.yml for tests
- All 265 tests pass
This commit is contained in:
Mondo Diaz
2026-01-08 18:18:29 -06:00
parent d61c7a71fb
commit 6aa199b80b
5 changed files with 30 additions and 1 deletions

View File

@@ -1,15 +1,19 @@
from fastapi import FastAPI from fastapi import FastAPI, Request
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
import logging import logging
import os import os
from slowapi import _rate_limit_exceeded_handler
from slowapi.errors import RateLimitExceeded
from .config import get_settings from .config import get_settings
from .database import init_db, SessionLocal from .database import init_db, SessionLocal
from .routes import router from .routes import router
from .seed import seed_database from .seed import seed_database
from .auth import create_default_admin from .auth import create_default_admin
from .rate_limit import limiter
settings = get_settings() settings = get_settings()
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
@@ -55,6 +59,10 @@ app = FastAPI(
lifespan=lifespan, lifespan=lifespan,
) )
# Set up rate limiting
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
# Include API routes # Include API routes
app.include_router(router) app.include_router(router)

16
backend/app/rate_limit.py Normal file
View File

@@ -0,0 +1,16 @@
"""Rate limiting configuration for Orchard API.
Uses slowapi for rate limiting with IP-based keys.
"""
import os
from slowapi import Limiter
from slowapi.util import get_remote_address
# Rate limiter - uses IP address as key
limiter = Limiter(key_func=get_remote_address)
# Rate limit strings - configurable via environment for testing
# Default: 5 login attempts per minute per IP
# In tests: set ORCHARD_LOGIN_RATE_LIMIT to a high value like "1000/minute"
LOGIN_RATE_LIMIT = os.environ.get("ORCHARD_LOGIN_RATE_LIMIT", "5/minute")

View File

@@ -374,9 +374,11 @@ from .auth import (
check_project_access, check_project_access,
AuthorizationService, AuthorizationService,
) )
from .rate_limit import limiter, LOGIN_RATE_LIMIT
@router.post("/api/v1/auth/login", response_model=LoginResponse) @router.post("/api/v1/auth/login", response_model=LoginResponse)
@limiter.limit(LOGIN_RATE_LIMIT)
def login( def login(
login_request: LoginRequest, login_request: LoginRequest,
request: Request, request: Request,

View File

@@ -10,6 +10,7 @@ pydantic-settings==2.1.0
python-jose[cryptography]==3.3.0 python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4 passlib[bcrypt]==1.7.4
bcrypt==4.0.1 bcrypt==4.0.1
slowapi==0.1.9
# Test dependencies # Test dependencies
pytest>=7.4.0 pytest>=7.4.0

View File

@@ -24,6 +24,8 @@ services:
- ORCHARD_S3_USE_PATH_STYLE=true - ORCHARD_S3_USE_PATH_STYLE=true
- ORCHARD_REDIS_HOST=redis - ORCHARD_REDIS_HOST=redis
- ORCHARD_REDIS_PORT=6379 - ORCHARD_REDIS_PORT=6379
# Higher rate limit for local development/testing
- ORCHARD_LOGIN_RATE_LIMIT=1000/minute
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy