- 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
4.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 rodaverify()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 desessions; o token anterior para de bater com qualquer hash no banco imediatamente. - Logout / revogação:
sessions.revoked_at— idempotente. mustChangePasswordnoUser: setadotrueno 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 é:
login()autentica só por e-mail/senha — devolve tokens sem tenant.listUserTenants(userId)devolve os tenants aos quais o usuário pertence (viatenant_memberships), usandowithUserContext— a única exceção documentada de RLS "olhar os próprios dados sem tenant ainda escolhido" (ver docs/TENANT_ISOLATION.md).setActiveTenant(sessionId, userId, tenantId)valida a membership de novo (nunca confia emtenantIdvindo do cliente sem checar) e emite um novo access token já comtenantIdnas claims.
RBAC (agente.md secoes 142-145)
Tabelas: roles, permissions, role_permissions, user_roles.
Role.scope:PLATFORM(vale em qualquer tenant,UserRole.tenantId = null) ouTENANT(vale só no tenant especificado emUserRole.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-limitou equivalente, não implementável empackages/authisoladamente. - Endpoints REST (
POST /auth/login,/auth/refresh,/auth/logout,/auth/select-tenant) e guards HTTP que traduzemInvalidCredentialsError/NotATenantMemberErrorem 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.