- 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
123 lines
6.5 KiB
Markdown
123 lines
6.5 KiB
Markdown
# 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.
|