sass-imobiliaria/.specify/features/005-authentication/research.md

135 lines
5.7 KiB
Markdown

# Research: 005-authentication
**Fase 0 — Resolução de Unknowns**
**Data**: 2026-04-13
---
## Decision Log
### 1. Biblioteca de hashing de senha
**Decision**: `bcrypt>=4.2`
**Rationale**: Bcrypt é o padrão da indústria para hashing de senhas — adaptativo, lento por design, resistente a ataques de força bruta com GPUs. Usar diretamente (sem passar por `werkzeug.security`) mantém a cadeia de dependências mínima (Princípio VI) e está alinhado com a instrução de design.
**Alternatives considered**:
- `werkzeug.security.generate_password_hash` — encapsula bcrypt mas adiciona dependência desnecessária
- `argon2-cffi` — mais forte que bcrypt, porém não justificado para MVP com ~100 usuários
- `passlib` — camada de abstração extra (YAGNI)
**Usage pattern**:
```python
import bcrypt
# Hash
hashed = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt())
password_hash = hashed.decode("utf-8") # armazenar como String(100)
# Verify
bcrypt.checkpw(password.encode("utf-8"), stored_hash.encode("utf-8"))
```
---
### 2. Biblioteca JWT
**Decision**: `PyJWT>=2.9`
**Rationale**: API limpa, amplamente adotada no ecossistema Flask, suporte nativo a expiração via claim `exp`. Leveza da dependência cumpre Princípio VI.
**Alternatives considered**:
- `python-jose` — orientado a JWE/JWKS, mais pesado (YAGNI)
- `authlib` — voltado a OAuth2 / OpenID Connect (fora do escopo)
- `flask-jwt-extended` — abstração extra útil para refresh tokens; sem refresh no escopo MVP, não se justifica
**Usage pattern**:
```python
import jwt
from datetime import datetime, timedelta
# Encode
payload = {"sub": str(user.id), "exp": datetime.utcnow() + timedelta(days=7)}
token = jwt.encode(payload, JWT_SECRET_KEY, algorithm="HS256")
# Decode (lança jwt.ExpiredSignatureError ou jwt.InvalidTokenError em falha)
data = jwt.decode(token, JWT_SECRET_KEY, algorithms=["HS256"])
```
---
### 3. UUID como chave primária do ClientUser
**Decision**: `default=uuid.uuid4` (gerado no Python)
**Rationale**: Consistência com o padrão já adotado no model `Property`. O ID gerado em Python fica disponível antes do flush, simplificando testes e a construção da resposta de registro.
**Alternatives considered**:
- `server_default=text("gen_random_uuid()")` — gerado no banco, menos consistente com padrão existente
---
### 4. Normalização de e-mail
**Decision**: `email.lower().strip()` como `@field_validator` nos schemas Pydantic (`RegisterIn` e `LoginIn`)
**Rationale**: FR-005 exige normalização para minúsculas. Fazê-la na camada de schema garante que o banco nunca armazene variantes de case, e que a comparação no login seja sempre consistente — sem precisar de `LOWER()` em queries SQL.
---
### 5. Armazenamento do token no frontend
**Decision**: `localStorage` com chave `imob_token`
**Rationale**: Escolha MVP pragmática. HttpOnly cookies seriam mais seguros contra XSS, mas exigiriam configuração adicional de CORS e CSRF. O projeto usa Axios com requisições XHR; consistência com o padrão SPA do projeto.
**Alternatives considered**:
- `sessionStorage` — perde estado ao fechar aba; viola SC-005
- HttpOnly cookie — mais seguro para produção, mas exigiria mudança na política CORS/CSRF (fora do escopo MVP)
**Risk noted**: Token em `localStorage` é vulnerável a XSS. Mitigação via CSP headers fica na responsabilidade de infraestrutura (fora do escopo desta feature). Documentado como débito de segurança para versão futura.
---
### 6. Axios interceptor vs. wrapper manual
**Decision**: `axios.interceptors.request.use` na instância existente em `services/api.ts`
**Rationale**: Centraliza a injeção do header `Authorization` em um único ponto. FR-015 é satisfeito sem nenhuma alteração nas chamadas existentes. Adicionar ao objeto Axios existente evita fragmentação de instâncias (Princípio VI).
**Alternatives considered**:
- Wrapper manual por serviço — duplicação de código
- Novo arquivo `axiosInstance.ts` — fragmenta a instância sem ganho
---
### 7. Estrutura do `require_auth` decorator
**Decision**: Decorator simples em `backend/app/utils/auth.py` usando `flask.g`
**Rationale**: Abordagem mínima e idiomática no Flask. `g` é o mecanismo nativo de context-local para guardar estado por requisição. Sem nova dependência.
**Pattern**:
```python
from functools import wraps
from flask import g, request, jsonify
import jwt
def require_auth(f):
@wraps(f)
def decorated(*args, **kwargs):
token = _extract_token(request)
if not token:
return jsonify({"error": "Token de acesso inválido ou expirado."}), 401
try:
payload = jwt.decode(token, JWT_SECRET_KEY, algorithms=["HS256"])
g.current_user = ClientUser.query.get(payload["sub"])
if g.current_user is None:
return jsonify({"error": "Token de acesso inválido ou expirado."}), 401
except jwt.PyJWTError:
return jsonify({"error": "Token de acesso inválido ou expirado."}), 401
return f(*args, **kwargs)
return decorated
```
---
### 8. `UserOut` sem `password_hash`
**Decision**: `UserOut` exclui `password_hash`
**Rationale**: Nunca expor hash de senha em resposta da API. Princípio de menor privilégio (Constituição V). `UserOut` = {id, name, email, role, created_at}.
---
### 9. `pydantic[email]` para `EmailStr`
**Decision**: Adicionar `pydantic[email]` nas dependências (ou verificar se `email-validator` já está presente)
**Rationale**: `pydantic.EmailStr` requer o pacote `email-validator`. Verificar `pyproject.toml` antes da tarefa de implementação — se ausente, adicionar junto com PyJWT e bcrypt.
**Action**: A tarefa de instalação de dependências deve incluir `"pydantic[email]"` ou checar se `email-validator>=2.0` já está listado.