Files
B2BCall-dialer/docs/AUTHENTICATION.md
Matheus 68b403a7ff feat: add apps/api (NestJS + Fastify) with authentication endpoints
- 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
2026-08-28 06:12:34 -03:00

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.