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