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

5.7 KiB

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:

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:

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:

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.