# 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.