Static Bearer Auth — Trial Register Endpoint
Branch infra:
feat/staticBearerAuth(Tasks 1–3) Branch aplicação:feat/UserTrialRegister(Task 4 — após merge da infra)
Goal: Proteger POST /v1/trials/register contra DDoS com um token estático pré-compartilhado enviado via Authorization: Bearer <token>.
Architecture: StaticBearerAuthentication implementa BaseAuthentication do DRF e valida o header contra settings.LANDING_PAGE_API_TOKEN. A permission IsStaticBearer checa se request.auth foi preenchido. Sem header → 403; token errado → 401; token correto → 201. As classes ficam em apps/common/ para serem reutilizáveis em qualquer endpoint futuro.
Tech stack: Django 5.2, Django REST Framework, python-decouple.
Global constraints
- Token lido via
decouple.config()emsettings/base.py— nunca hardcoded. - Sem user model envolvido:
authenticate()retorna(None, token). - Testes de common ficam em
tests/common/, testes de view emtests/trials/test_view.py.
Task 1: Adicionar LANDING_PAGE_API_TOKEN ao settings e .env.example
Files:
- Modify: config/settings/base.py
- Modify: .env.example
- [ ] Step 1: Escrever o teste que verifica que a setting existe
```python # tests/test_settings.py (novo) from django.conf import settings
def test_landing_page_api_token_setting_exists(): assert hasattr(settings, “LANDING_PAGE_API_TOKEN”) assert isinstance(settings.LANDING_PAGE_API_TOKEN, str) assert len(settings.LANDING_PAGE_API_TOKEN) > 0 ```
- [ ] Step 2: Rodar para verificar que falha
bash
python manage.py test tests.test_settings.test_landing_page_api_token_setting_exists
Expected: FAIL — AttributeError: module 'django.conf.settings' has no attribute 'LANDING_PAGE_API_TOKEN'
- [ ] Step 3: Implementar
Em config/settings/base.py, junto com as outras chaves de segurança:
python
LANDING_PAGE_API_TOKEN = config('LANDING_PAGE_API_TOKEN', default='insecure-token-troque-em-producao', cast=str)
Em .env.example, na seção de integrações externas:
# ─────────────────────────────────────────────
# Landing Page
# ─────────────────────────────────────────────
LANDING_PAGE_API_TOKEN=secure-token
- [ ] Step 4: Rodar para verificar que passa
bash
python manage.py test tests.test_settings.test_landing_page_api_token_setting_exists
Expected: PASS
- [ ] Step 5: Commit
bash
git add config/settings/base.py .env.example tests/test_settings.py
git commit -m "feat: add LANDING_PAGE_API_TOKEN setting"
Task 2: Criar StaticBearerAuthentication
Files:
- Create: apps/common/authentication.py
- Test: tests/common/test_authentication.py
Interfaces:
- Produces: StaticBearerAuthentication — usado pela Task 4 na view
- [ ] Step 1: Escrever os testes que falham
```python # tests/common/test_authentication.py (novo — criar tests/common/init.py também) import pytest from django.test import override_settings from rest_framework.exceptions import AuthenticationFailed from rest_framework.test import APIRequestFactory
from apps.common.authentication import StaticBearerAuthentication
@override_settings(LANDING_PAGE_API_TOKEN=”test-token-123”) def test_authenticate_returns_none_when_no_authorization_header(): request = APIRequestFactory().post(“/”) result = StaticBearerAuthentication().authenticate(request) assert result is None
@override_settings(LANDING_PAGE_API_TOKEN=”test-token-123”) def test_authenticate_raises_when_token_is_wrong(): request = APIRequestFactory().post(“/”, HTTP_AUTHORIZATION=”Bearer wrong-token”) with pytest.raises(AuthenticationFailed): StaticBearerAuthentication().authenticate(request)
@override_settings(LANDING_PAGE_API_TOKEN=”test-token-123”) def test_authenticate_returns_none_user_and_token_when_valid(): request = APIRequestFactory().post(“/”, HTTP_AUTHORIZATION=”Bearer test-token-123”) user, auth = StaticBearerAuthentication().authenticate(request) assert user is None assert auth == “test-token-123”
def test_authenticate_header_returns_bearer(): request = APIRequestFactory().post(“/”) result = StaticBearerAuthentication().authenticate_header(request) assert result == “Bearer” ```
- [ ] Step 2: Rodar para verificar que falha
bash
python manage.py test tests.common.test_authentication
Expected: FAIL — ModuleNotFoundError: No module named 'apps.common.authentication'
- [ ] Step 3: Implementar
```python # apps/common/authentication.py from django.conf import settings from rest_framework.authentication import BaseAuthentication from rest_framework.exceptions import AuthenticationFailed
class StaticBearerAuthentication(BaseAuthentication): def authenticate(self, request): auth_header = request.META.get(“HTTP_AUTHORIZATION”, “”) if not auth_header.startswith(“Bearer “): return None token = auth_header.split(“ “, 1)[1] if token != settings.LANDING_PAGE_API_TOKEN: raise AuthenticationFailed(“Invalid token.”) return (None, token)
def authenticate_header(self, request):
return "Bearer" ```
- [ ] Step 4: Rodar para verificar que passa
bash
python manage.py test tests.common.test_authentication
Expected: PASS
- [ ] Step 5: Commit
bash
git add apps/common/authentication.py tests/common/__init__.py tests/common/test_authentication.py
git commit -m "feat: add StaticBearerAuthentication to common"
Task 3: Criar IsStaticBearer permission
Files:
- Create: apps/common/permissions.py
- Test: tests/common/test_permissions.py
Interfaces:
- Consumes: StaticBearerAuthentication (Task 2) — request.auth preenchido por ela
- Produces: IsStaticBearer — usado pela Task 4 na view
- [ ] Step 1: Escrever os testes que falham
```python # tests/common/test_permissions.py (novo) from unittest.mock import MagicMock
from apps.common.permissions import IsStaticBearer
def test_permission_denied_when_request_auth_is_none(): request = MagicMock() request.auth = None assert IsStaticBearer().has_permission(request, view=None) is False
def test_permission_granted_when_request_auth_is_set(): request = MagicMock() request.auth = “test-token-123” assert IsStaticBearer().has_permission(request, view=None) is True ```
- [ ] Step 2: Rodar para verificar que falha
bash
python manage.py test tests.common.test_permissions
Expected: FAIL — ModuleNotFoundError: No module named 'apps.common.permissions'
- [ ] Step 3: Implementar
```python # apps/common/permissions.py from rest_framework.permissions import BasePermission
class IsStaticBearer(BasePermission): def has_permission(self, request, view): return request.auth is not None ```
- [ ] Step 4: Rodar para verificar que passa
bash
python manage.py test tests.common.test_permissions
Expected: PASS
- [ ] Step 5: Commit
bash
git add apps/common/permissions.py tests/common/test_permissions.py
git commit -m "feat: add IsStaticBearer permission to common"
Task 4: Aplicar autenticação na view e atualizar testes
Files:
- Modify: apps/trials/views.py
- Modify: tests/trials/test_view.py
Interfaces:
- Consumes: StaticBearerAuthentication (Task 2), IsStaticBearer (Task 3)
- [ ] Step 1: Atualizar os testes da view
```python # tests/trials/test_view.py (substituir conteúdo) from unittest.mock import patch
import pytest from django.test import override_settings from rest_framework.test import APIRequestFactory
from apps.common.authentication import StaticBearerAuthentication from apps.common.permissions import IsStaticBearer from apps.trials.views import UserTrialRegisterView
TOKEN = “test-token-abc”
def _post(data, token=None): kwargs = {“format”: “json”} if token: kwargs[“HTTP_AUTHORIZATION”] = f”Bearer {token}” return APIRequestFactory().post(“/v1/trials/register”, data, **kwargs)
def test_register_view_uses_static_bearer_authentication(): assert StaticBearerAuthentication in UserTrialRegisterView.authentication_classes
def test_register_view_uses_is_static_bearer_permission(): assert IsStaticBearer in UserTrialRegisterView.permission_classes
@override_settings(LANDING_PAGE_API_TOKEN=TOKEN) def test_register_view_returns_403_without_token(): request = _post({“email”: “a@test.com”, “phone”: “11999999999”, “slug”: “slug”}) response = UserTrialRegisterView.as_view()(request) assert response.status_code == 403
@override_settings(LANDING_PAGE_API_TOKEN=TOKEN) def test_register_view_returns_401_with_wrong_token(): request = _post( {“email”: “a@test.com”, “phone”: “11999999999”, “slug”: “slug”}, token=”wrong-token”, ) response = UserTrialRegisterView.as_view()(request) assert response.status_code == 401
@override_settings(LANDING_PAGE_API_TOKEN=TOKEN) def test_register_view_returns_400_on_missing_fields(): request = _post({}, token=TOKEN) response = UserTrialRegisterView.as_view()(request) assert response.status_code == 400
@override_settings(LANDING_PAGE_API_TOKEN=TOKEN) def test_register_view_returns_400_on_invalid_email(): request = _post( {“email”: “nao-e-email”, “phone”: “11999999999”, “slug”: “slug”}, token=TOKEN, ) response = UserTrialRegisterView.as_view()(request) assert response.status_code == 400
@pytest.mark.django_db @override_settings(LANDING_PAGE_API_TOKEN=TOKEN) def test_register_view_returns_201_on_success(): request = _post( {“email”: “novo@test.com”, “phone”: “11999999999”, “slug”: “slug”}, token=TOKEN, ) with patch(“apps.trials.serializer.UserTrialService.register_user_trial”, return_value=object()): response = UserTrialRegisterView.as_view()(request) assert response.status_code == 201 ```
- [ ] Step 2: Rodar para verificar que falha
bash
python manage.py test tests.trials.test_view
Expected: FAIL — AssertionError nos testes de autenticação/permissão
- [ ] Step 3: Implementar
```python # apps/trials/views.py from rest_framework import status from rest_framework.response import Response from rest_framework.views import APIView
from apps.common.authentication import StaticBearerAuthentication from apps.common.permissions import IsStaticBearer
from .serializer import UserTrialRegisterSerializer
class UserTrialRegisterView(APIView): authentication_classes = [StaticBearerAuthentication] permission_classes = [IsStaticBearer] serializer_class = UserTrialRegisterSerializer
def post(self, request):
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(status=status.HTTP_201_CREATED) ```
- [ ] Step 4: Rodar para verificar que passa
bash
python manage.py test tests.trials.test_view
Expected: PASS
- [ ] Step 5: Commit
bash
git add apps/trials/views.py tests/trials/test_view.py
git commit -m "feat: protect trial register with static bearer token"
Verificação final
bash
python manage.py test tests.common tests.trials.test_view tests.test_settings
python manage.py check
Todos os testes devem passar e check sem erros.