- POST /auth/login, /auth/refresh, /auth/logout, /auth/select-tenant, /auth/change-password, GET /auth/tenants — wired to packages/auth - JwtAuthGuard + DomainExceptionFilter (401/403 without leaking internals) - LoginRateLimitGuard: Redis-backed 5/min per IP and per email (agente.md secao 149), safe across multiple API instances - helmet + restrictive cors (deny-by-default) + global rate limit - GET /health, /health/live, /health/ready checking Postgres and Redis - changePassword() added to packages/auth for the mustChangePassword flow - fixed REDIS_HOST/POSTGRES_HOST docker-compose-only hostnames not resolving from the host process; added REDIS_URL for host-side use - verified end-to-end with curl: login, wrong password / unknown email (same generic error), authenticated route, missing token, refresh rotation, logout revocation, and the 429 rate limit kicking in after 5 attempts
6.5 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.
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 injetarequest.user(claims do JWT);@CurrentUser()expõe isso no controller.DomainExceptionFiltertraduz os erros depackages/auth(InvalidCredentialsError→ 401,InvalidRefreshTokenError→ 401,NotATenantMemberError→ 403) sem vazar stack trace.LoginRateLimitGuardconta 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+corsrestritivo (CORS_ORIGINvia env,falsepor 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) —readycheca 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
mustChangePasswordbloqueando outras rotas (hoje só o client precisa respeitar a flag) — revisitar quando existirem rotas de negócio de verdade para proteger.