Auth & RBAC
Sign-in is provider-based (no local passwords by default), roles gate what you can do, and a visibility model gates what you can see. The API trusts identity only from the frontend proxy, sealed with a shared secret.
Sign-in providers
Authentication is NextAuth (Auth.js v5), entirely inside the Next.js app — no external gateway. Each provider activates automatically when its credentials are present in the environment:
| Provider | Env vars |
|---|---|
| GitHub | GITHUB_ID · GITHUB_SECRET |
GOOGLE_ID · GOOGLE_SECRET | |
| Microsoft Entra ID | AUTH_MICROSOFT_ENTRA_ID_ID · _SECRET · _ISSUER |
Also required: AUTH_SECRET (session signing). The OAuth callback URL is {origin}/api/auth/callback/{provider} — register it on each provider app.
https://kaveon.vercel.app/…and http://localhost:3000/…). A missing origin is the usual cause of a “redirect_uri not associated” error.Roles
Four roles, ordered by privilege, gate what you can do in the API. Through sign-in a user resolves to Admin (email in AUTH_ADMIN_EMAILS) or Viewer:
| Role | Can |
|---|---|
| Viewer | Read published dashboards and charts |
| Analyst | + Run SQL, build charts and datasets |
| Editor | + Publish content, delete |
| Admin | + Manage users, data sources, metadata server, AI keys |
Admins are seeded from AUTH_ADMIN_EMAILS (comma-separated). Every state-changing endpoint is guarded by a require_min_role("…") dependency — a lower role gets 403 forbidden.
Viewer / Analyst / Editor / Admin). The NextAuth sign-in itself resolves each user to just two of them: Admin when their email is listed in AUTH_ADMIN_EMAILS, otherwise Viewer.Visibility
Independent of role, each dataset, chart, and dashboard has a visibility level — private (owner), internal (signed-in users), published — enforced by a can_read()helper on every list/get call.
The proxy trust contract
The browser never holds an API token. The Next.js proxy is the only ingress and stamps identity headers server-side:
kaveon-web /api/kaveon/[...path]
├─ reads the NextAuth session (server-side)
├─ injects X-User-Email · X-User-Name · X-User-Role
└─ signs the request with X-Proxy-Secret = KAVEON_PROXY_SECRET
▼
kaveon-api trusts X-User-* ONLY when the secret matchesThe same KAVEON_PROXY_SECRET must be set on both the web and API deployments.
Hardening built in
- Identifier injection — all user identifiers are quoted/escaped before hitting SQL.
- Operator injection — filter operators are validated against a strict allowlist.
- Credential exposure — the
connection_stringcolumn is stripped from every API response. - Identity spoofing — raw
x-user-emailheaders are rejected unless the proxy secret matches. - Information disclosure — internal errors are logged server-side; clients get a generic message.