Files
B2BCall-dialer/docs/AUTHENTICATION.md
Matheus 5c23b105b3 feat(auth): aumenta sessão web pra 12h (pedido do usuário: call center, agente não pode cair no meio do turno)
Access token JWT (packages/auth/src/tokens.ts) e o teto absoluto do cookie
de sessão (duplicado em login/post-login/select-tenant/middleware) subiram
de 15min/8h pra 12h. O refresh silencioso do middleware continua existindo
e deslizando a sessão pra frente a cada requisição — 12h agora é só o teto
de segurança pra quando esse refresh falhar (aba parada, logout em outro
lugar), não o intervalo real de renovação.

Verificado com login real: token decodificado mostra exp-iat = 12h exatas.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
2026-08-31 12:48:33 -03:00

6.7 KiB

Autenticação e RBAC

Implementado em packages/auth, sobre o schema criado pela migration auth_and_rbac (packages/database/prisma/schema.prisma).

Login (agente.md secao 148)

  • Senha: Argon2id via @node-rs/argon2 (binário pré-compilado, sem node-gyp), parâmetros padrão OWASP (memoryCost 19 MiB, timeCost 2, parallelism 1).
  • login() sempre roda verify() contra um hash Argon2id fixo mesmo quando o e-mail não existe, e devolve o mesmo erro genérico (InvalidCredentialsError) em qualquer caso de falha — mitiga user-enumeration por diferença de tempo de resposta ou mensagem.
  • Access token: JWT (HS256, jose), TTL 12 horas (era 15 minutos até a PHASE 69 — pedido do usuário: turno de agente de call center não pode cair no meio, o refresh silencioso do middleware existe mas 15min forçava ele rodar a cada poucos minutos), claims { sub, sessionId, tenantId? }.
  • Refresh token: string opaca aleatória (32 bytes), nunca JWT. Só o SHA-256 fica salvo em sessions.refresh_token_hash — posse do token original é a prova de identidade.
  • Refresh rotation: cada uso de refreshSession() troca o hash guardado na mesma linha de sessions; o token anterior para de bater com qualquer hash no banco imediatamente.
  • Logout / revogação: sessions.revoked_at — idempotente.
  • mustChangePassword no User: setado true no Platform Super Admin inicial (seed); a aplicação (quando existir a camada HTTP) deve forçar troca de senha antes de liberar qualquer outra rota quando essa flag estiver ativa.

Resolução de tenant (agente.md secao 31)

Login não recebe nem decide tenant_id. O fluxo é:

  1. login() autentica só por e-mail/senha — devolve tokens sem tenant.
  2. listUserTenants(userId) devolve os tenants aos quais o usuário pertence (via tenant_memberships), usando withUserContext — a única exceção documentada de RLS "olhar os próprios dados sem tenant ainda escolhido" (ver docs/TENANT_ISOLATION.md).
  3. setActiveTenant(sessionId, userId, tenantId) valida a membership de novo (nunca confia em tenantId vindo do cliente sem checar) e emite um novo access token já com tenantId nas claims.

RBAC (agente.md secoes 142-145)

Tabelas: roles, permissions, role_permissions, user_roles.

  • Role.scope: PLATFORM (vale em qualquer tenant, UserRole.tenantId = null) ou TENANT (vale só no tenant especificado em UserRole.tenantId).
  • userHasPermission(userId, permissionKey, tenantId?): junta roles PLATFORM do usuário com as roles TENANT do tenant informado, e checa se alguma delas carrega a permission. Chamado explicitamente pela camada de serviço — não depende de RLS (ver próxima seção).
  • Seed (packages/auth/src/seed.ts, agente.md secao 200) cria o catálogo de permissions e 4 roles de sistema: platform_super_admin (PLATFORM, todas as permissions), tenant_admin, supervisor, agent (TENANT, mapeamento inicial documentado no próprio seed — ajustar quando existir UI de RBAC).

Por que roles/permissions/user_roles/sessions/audit_logs NÃO têm RLS

Diferente das tabelas de negócio tenant-scoped (extensions, agents, campaigns, calls, ...), essas tabelas são infraestrutura de autenticação/autorização, tocadas exclusivamente pelo código confiável de packages/auth, que já resolve o filtro de tenant explicitamente em cada query (userHasPermission, setActiveTenant, etc.). Isso é a camada "RBAC + object authorization" da defesa em profundidade da seção 32 do agente.md — complementar à RLS, não substituída por ela. Se no futuro essas tabelas passarem a ser expostas por queries genéricas (ex.: um endpoint de admin que aceita filtros arbitrários), revisitar essa decisão e considerar RLS ali também.

Platform Super Admin inicial (agente.md secao 199)

O seed cria admin@b2bcall.local com senha aleatória de 24 bytes, salva uma única vez em FIRST_LOGIN.txt (permissão 600, fora do Git) com mustChangePassword = true. Rodar de novo o seed não recria o admin se já existir um usuário com role platform_super_admin.

Camada HTTP (apps/api)

NestJS sobre Fastify (agente.md secao 12). Rotas em apps/api/src/auth:

POST /auth/login           { email, password } -> { accessToken, refreshToken, sessionId, mustChangePassword }
POST /auth/refresh         { refreshToken }     -> { accessToken, refreshToken }
POST /auth/logout          (autenticado)        -> 204
GET  /auth/tenants         (autenticado)        -> [{ tenantId, code, name, status }]
POST /auth/select-tenant   (autenticado) { tenantId } -> { accessToken }
POST /auth/change-password (autenticado) { currentPassword, newPassword } -> 204
  • JwtAuthGuard (apps/api/src/common/guards) valida o access token e injeta request.user (claims do JWT); @CurrentUser() expõe isso no controller.
  • DomainExceptionFilter traduz os erros de packages/auth (InvalidCredentialsError → 401, InvalidRefreshTokenError → 401, NotATenantMemberError → 403) sem vazar stack trace.
  • LoginRateLimitGuard conta tentativas de login por IP e por e-mail via Redis (INCR+EXPIRE, 5/minuto — agente.md secao 149), funcionando corretamente com múltiplas instâncias da API (ao contrário de um contador em memória local).
  • helmet + cors restritivo (CORS_ORIGIN via env, false por padrão — nega tudo até ser configurado) + rate limit global de 300/min como defesa extra (agente.md secao 182).
  • GET /health, /health/live, /health/ready (agente.md secao 187) — ready checa Postgres e Redis de verdade.
  • API escuta só em 127.0.0.1:3000 (API_PORT) — nunca exposta direto, fica atrás do nginx quando ele existir.

Pegadinha de rede corrigida durante os testes: REDIS_HOST=redis / POSTGRES_HOST=postgres no .env são os nomes de serviço do Docker Compose — só resolvem de dentro da rede Docker. Como apps/api roda direto no host (ainda não containerizada), ela usa REDIS_URL/APP_DATABASE_URL (@localhost) em vez disso. Quando b2bcall-api virar um serviço Docker na mesma rede, essas URLs precisam trocar para os hostnames internos.

O que falta

  • Password reset (link expirável por e-mail) — precisa de um provedor de e-mail/SMTP, fora do escopo desta fase.
  • Reuse detection de refresh token roubado (família de tokens) — não implementado; a rotação simples (secao 148) já está feita, a detecção de reuso é um hardening adicional a avaliar depois.
  • Progressive blocking (backoff exponencial) no login — hoje é só janela fixa de 5/minuto.
  • Enforcement server-side de mustChangePassword bloqueando outras rotas (hoje só o client precisa respeitar a flag) — revisitar quando existirem rotas de negócio de verdade para proteger.