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áriaargon2-cffi— mais forte que bcrypt, porém não justificado para MVP com ~100 usuáriospasslib— 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.