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() em settings/base.py — nunca hardcoded.
  • Sem user model envolvido: authenticate() retorna (None, token).
  • Testes de common ficam em tests/common/, testes de view em tests/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.