mirror of
https://github.com/dartdavros/chatballs.git
synced 2026-10-11 20:04:59 +03:00
🔒 fix(security): rate-limit sensitive auth endpoints (#4)
- DRF ScopedRateThrottle on login (10/min), password-reset request/confirm (5/min) and TOTP verify (10/min); rates configurable, disabled under tests - throttling backed by the Redis cache in production - test asserts login is rejected with 429 after the limit Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
f5bec6bf77
commit
6ed2c175ef
3 files changed
+43
-1
No files matched your search
@@ -162,12 +162,18 @@ STORAGES = {
|
||||
}
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
# Лимиты на чувствительные эндпоинты (брутфорс/злоупотребление). В тестах отключены.
|
||||
_THROTTLE_RATES = {"login": "10/min", "password_reset": "5/min", "totp": "10/min"}
|
||||
if TESTING:
|
||||
_THROTTLE_RATES = {scope: None for scope in _THROTTLE_RATES}
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
"DEFAULT_RENDERER_CLASSES": ["rest_framework.renderers.JSONRenderer"],
|
||||
"DEFAULT_PARSER_CLASSES": ["rest_framework.parsers.JSONParser"],
|
||||
"DEFAULT_AUTHENTICATION_CLASSES": ["rest_framework.authentication.SessionAuthentication"],
|
||||
"DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"],
|
||||
"EXCEPTION_HANDLER": "hub_platform.api.exceptions.api_exception_handler",
|
||||
"DEFAULT_THROTTLE_RATES": _THROTTLE_RATES,
|
||||
}
|
||||
|
||||
CORS_ALLOWED_ORIGINS = env_list(
|
||||
|
||||
@@ -20,6 +20,7 @@ from django.views.decorators.csrf import csrf_protect, ensure_csrf_cookie
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.throttling import ScopedRateThrottle
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from hub_platform.identity.audit import record_audit_event
|
||||
@@ -148,6 +149,8 @@ class SessionView(APIView):
|
||||
class LoginView(APIView):
|
||||
authentication_classes: list = []
|
||||
permission_classes = [AllowAny]
|
||||
throttle_classes = [ScopedRateThrottle]
|
||||
throttle_scope = "login"
|
||||
|
||||
def post(self, request: Request) -> Response:
|
||||
body = request.data
|
||||
@@ -205,6 +208,8 @@ class LogoutView(APIView):
|
||||
class PasswordResetRequestView(APIView):
|
||||
authentication_classes: list = []
|
||||
permission_classes = [AllowAny]
|
||||
throttle_classes = [ScopedRateThrottle]
|
||||
throttle_scope = "password_reset"
|
||||
|
||||
def post(self, request: Request) -> Response:
|
||||
email = HumanUser.objects.normalize_email(str(request.data.get("email", "")).strip())
|
||||
@@ -244,6 +249,8 @@ class PasswordResetValidateView(APIView):
|
||||
class PasswordResetConfirmView(APIView):
|
||||
authentication_classes: list = []
|
||||
permission_classes = [AllowAny]
|
||||
throttle_classes = [ScopedRateThrottle]
|
||||
throttle_scope = "password_reset"
|
||||
|
||||
def post(self, request: Request) -> Response:
|
||||
body = request.data
|
||||
@@ -479,6 +486,8 @@ class TotpConfirmView(APIView):
|
||||
class TotpVerifyView(APIView):
|
||||
authentication_classes: list = []
|
||||
permission_classes = [AllowAny]
|
||||
throttle_classes = [ScopedRateThrottle]
|
||||
throttle_scope = "totp"
|
||||
|
||||
def post(self, request: Request) -> Response:
|
||||
pending_user_id = request.session.get(TOTP_SESSION_KEY)
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import json
|
||||
from unittest import mock
|
||||
|
||||
from django.contrib.auth.tokens import default_token_generator
|
||||
from django.core import mail
|
||||
from django.test import Client, TestCase
|
||||
from django.test import Client, TestCase, override_settings
|
||||
from django.contrib.sessions.backends.db import SessionStore
|
||||
from django.utils.encoding import force_bytes
|
||||
from django.utils.http import urlsafe_base64_encode
|
||||
from rest_framework.test import APIClient
|
||||
from rest_framework.throttling import ScopedRateThrottle
|
||||
|
||||
_LOCMEM_CACHE = {"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}}
|
||||
|
||||
from hub_platform.identity.bootstrap import bootstrap_edevs_owner
|
||||
from hub_platform.identity.auth_views import _totp_code
|
||||
@@ -557,3 +561,26 @@ class TotpSecretEncryptionTests(TestCase):
|
||||
|
||||
profile.refresh_from_db()
|
||||
self.assertEqual(profile.totp_secret, "JBSWY3DPEHPK3PXP")
|
||||
|
||||
|
||||
@override_settings(CACHES=_LOCMEM_CACHE)
|
||||
class ThrottlingTests(TestCase):
|
||||
def setUp(self) -> None:
|
||||
bootstrap_edevs_owner(email="owner@edevs.tech", password="temporary-password")
|
||||
self.client = Client()
|
||||
|
||||
def _login(self):
|
||||
return self.client.post(
|
||||
"/api/v1/auth/login/",
|
||||
data=json.dumps({"email": "owner@edevs.tech", "password": "wrong"}),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
def test_login_endpoint_is_rate_limited(self) -> None:
|
||||
# DRF биндит THROTTLE_RATES на импорте, поэтому ставим лимит напрямую.
|
||||
with mock.patch.dict(ScopedRateThrottle.THROTTLE_RATES, {"login": "1/min"}):
|
||||
first = self._login()
|
||||
second = self._login()
|
||||
|
||||
self.assertEqual(first.status_code, 401)
|
||||
self.assertEqual(second.status_code, 429)
|
||||
Reference in new issue
Block a user