feat: implement authentication and RBAC
- packages/auth: Argon2id password hashing, JWT access tokens (jose), opaque refresh tokens with rotation, generic error messages (no user-enumeration via timing or message differences) - roles/permissions/role_permissions/user_roles/sessions/audit_logs schema (agente.md secoes 142-150); RBAC scope PLATFORM vs TENANT - withUserContext(): narrow RLS exception so a user can discover their own tenant_memberships before a tenant is chosen (login flow) - userHasPermission()/isPlatformUser(): explicit service-layer RBAC checks (roles/permissions tables are not RLS-protected — documented why in docs/AUTHENTICATION.md) - seed: permission catalog, 4 system roles, initial Platform Super Admin (password written once to FIRST_LOGIN.txt, 600, outside Git) - automated end-to-end test: login, RBAC check, refresh rotation, logout
This commit is contained in:
88
docs/AUTHENTICATION.md
Normal file
88
docs/AUTHENTICATION.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# 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`.
|
||||
|
||||
## O que falta (fica para quando existir `apps/api`)
|
||||
|
||||
Tudo abaixo depende de uma camada HTTP (NestJS/Fastify) que ainda não existe:
|
||||
|
||||
- Rate limiting de login por IP/usuário (agente.md secao 149) — plugin
|
||||
`@fastify/rate-limit` ou equivalente, não implementável em `packages/auth`
|
||||
isoladamente.
|
||||
- Endpoints REST (`POST /auth/login`, `/auth/refresh`, `/auth/logout`,
|
||||
`/auth/select-tenant`) e guards HTTP que traduzem `InvalidCredentialsError`/
|
||||
`NotATenantMemberError` em 401/403.
|
||||
- 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.
|
||||
Reference in New Issue
Block a user