# ADR-0032: Cloud Authentication — External Identity Provider (Auth0) ## Metadata | Field | Value | |-------|-------| | **Status** | Superseded (see ADR-0062) | | **Date** | 2026-06-26 | | **Authors** | AlessioBrillo | | **Deciders** | AlessioBrillo | | **Supersedes** | ADR-0017 (extends — community edition retains env-var API keys) | | **Relates to** | ADR-0027, ADR-0034, ADR-0062 | | **Project** | DOMINUS | ## Context DOMINUS currently authenticates API requests via a static API key stored in the `API_KEYS` environment variable (`EnvApiKeyProvider`), as documented in ADR-0017. This model works for the community edition (single-user, localhost or LAN deployment) but has five fundamental limitations that block DOMINUS Cloud: 1. **No user registration or login flow** — Users cannot sign up, log in, or manage their account without editing environment variables. 2. **No tenant resolution** — The current auth middleware has no concept of `tenant_id`. Cloud requires extracting the tenant from the authentication context and injecting it into every database query (see ADR-0034). 3. **No role-based access** — Every authenticated request is equivalent. Cloud needs at least `admin` and `member` roles per tenant. 4. **No key rotation without restart** — Changing the API key requires restarting the process, which is unacceptable for a multi-tenant cloud service. 5. **No session management** — No token expiry, refresh tokens, or revocation support. The `AuthProvider` interface already exists in `src/providers/auth/auth-provider.ts` as a clean abstraction. This ADR adds a second implementation (`Auth0Provider`) for DOMINUS Cloud while preserving the existing `EnvApiKeyProvider` unchanged for the community edition. The choice of *which* provider to use is made at startup via the `AUTH_PROVIDER` configuration variable, wired in the composition root (`src/app/provider-factory.ts`). ## Decision Drivers 1. **Security surface reduction** — Handling password hashing, email verification, MFA, and OAuth flows in-house is a significant security liability for a solo-founder project. An external IdP offloads this to a SOC2-compliant platform. 2. **Multi-tenant token architecture** — The JWT must encode `sub` (user ID), `org_id` (tenant ID), and `role` (admin/member). The middleware extracts these claims and injects them into the request context without additional database lookups. 3. **Community edition unchanged** — The `EnvApiKeyProvider` must continue to work identically. No existing user should be forced to adopt Auth0. The two providers coexist via the existing interface. 4. **Cost discipline** — The external IdP must have a viable free tier for small teams. Auth0's free tier (7,500 active users, unlimited tenants) covers DOMINUS Cloud's expected launch scale. 5. **API key support for CLI** — The cloud edition must still support API keys for CLI access (many users prefer terminal workflows). These are generated per-user, stored as bcrypt hashes, and scoped to a tenant. ## Considered Options ### Option A: Auth0 — External OpenID Connect Provider Auth0 is a managed identity platform that supports OAuth 2.0, OpenID Connect, social logins, passwordless, and enterprise SSO. The application delegates authentication to Auth0 and validates JWTs via JWKS endpoint. **Advantages:** - SOC2, ISO 27001, HIPAA compliant — password hashing, MFA, breach detection managed by Auth0 - 7,500 free active users — covers the entire expected launch phase - JWKS-based JWT validation — no outbound API calls to Auth0 on every request (key rotation is automatic) - Built-in support for social login (Google, GitHub, Apple) — reduces friction for self-serve signup - Extensible with Actions/Rules — custom claims (tenant ID, role) added to tokens via Auth0 Actions - Well-documented Node.js SDK (`express-oauth2-jwt-bearer`, `auth0` npm packages) - Tenant resolution via Auth0 Organizations feature — `org_id` claim in JWT, org management via Auth0 Dashboard - Rate limiting and brute-force protection managed by Auth0 **Disadvantages:** - External dependency — if Auth0 has an outage, login is blocked (mitigated by JWT validation being offline after initial JWKS fetch) - Vendor lock-in — migrating away from Auth0 requires changing the JWT validation logic and user migration - Configuration complexity — Auth0 Dashboard requires tenant setup, application registration, API definition, Actions for custom claims, and organization setup - Free tier limits: 7,500 MAU, 2 social connections, 1 enterprise connection — may require upgrade at scale - Auth0 Actions for custom claims run on their infrastructure — adds ~50-200ms to login flow (one-time cost per session) **Cost Implications:** Free tier (7,500 MAU). Paid tier starts at ~€23/month for 1,000 MAU with all features. Estimated Cloud launch cost: €0/month. **Risk Assessment:** Low-Medium. Auth0 is a mature platform with 99.99%+ uptime. The JWT validation path is offline after the initial JWKS fetch, so a brief Auth0 outage doesn't block authenticated API requests. Token refresh would fail during an outage, requiring re-login after token expiry (configurable, default 24h for refresh tokens). --- ### Option B: Clerk — External Identity Platform Clerk is a newer identity platform with a developer-focused API, React/Next.js SDKs, and built-in organization (tenant) management. **Advantages:** - First-class React SDK with hooks (`useUser`, `useAuth`, `useOrganization`) - Built-in organization management — no Auth0 Actions needed for multi-tenant claims - Simpler setup — less configuration surface than Auth0 - Free tier: 10,000 MAU, unlimited organizations — more generous than Auth0 - Webhook support for user lifecycle events (signup, delete, org membership change) **Disadvantages:** - Smaller ecosystem — fewer SDKs, fewer community resources than Auth0 - No Node.js Express native middleware — requires calling Clerk's API or building a custom JWT validation layer - JWT validation requires an API call to Clerk unless using their SDK middleware (which is Next.js-specific) - CLI API key model would need custom implementation — Clerk doesn't natively support long-lived API keys - Less mature for B2B use cases — organizations feature is newer than Auth0's - Vendor risk — smaller company, less established than Auth0 (Okta) **Cost Implications:** Free tier (10,000 MAU, unlimited orgs). Paid tier starts at ~€20/month. **Risk Assessment:** Medium. Clerk is growing fast but lacks Express-native JWT validation. The React-first model conflicts with DOMINUS's client-side SPA architecture where auth is handled by the Express backend, not React. Would require building a custom JWT validation middleware that calls Clerk's JWKS endpoint. --- ### Option C: Self-Hosted JWT with bcrypt + refresh tokens Build a complete authentication system from scratch: user registration, login, password hashing (bcrypt), JWT generation, refresh token rotation, email verification, and MFA. **Advantages:** - Zero external dependencies — no third-party service required - Full control over the auth flow — custom claims, custom token format, custom policies - No per-user fees — unlimited users at €0 operational cost - No vendor lock-in - Community edition can optionally use the same system **Disadvantages:** - Massive security surface — password hashing, timing attacks, token theft, CSRF, email verification, rate limiting on login, brute-force protection — all must be correctly implemented - Estimated 80-120h of development for a production-grade auth system (registration, login, password reset, email verification, MFA, session management, API key management, admin panel) - Ongoing maintenance — security patches, vulnerability monitoring, dependency updates - Solo-founder risk — a single security vulnerability in the auth system could expose all tenant data - Email delivery — requires integrating SendGrid/Mailgun/SES for verification and password reset emails (adds cost and complexity) - No social login without building OAuth clients for each provider **Cost Implications:** €0 operational cost. ~100h development time. Email delivery: ~€10-20/month for transactional emails at scale. **Risk Assessment:** High. The solo-founder security burden is the deciding factor. Authentication is the most security-critical subsystem — a vulnerability here compromises all tenants. The cost of getting it wrong far exceeds the cost of Auth0's paid tier. --- ### Option D: Supabase Auth Supabase offers a hosted Auth service as part of their PostgreSQL platform. It uses GoTrue (the open-source auth server behind Netlify Identity) and supports JWT, social login, and Row-Level Security integration. **Advantages:** - Tightly integrated with PostgreSQL — RLS policies use `auth.uid()` and `auth.jwt()` directly - Supabase provides database, auth, and storage in one platform - Open-source GoTrue server — can be self-hosted if needed - Free tier: 50,000 MAU, unlimited projects - Well-documented PostgreSQL + RLS integration **Disadvantages:** - Supabase Auth is PostgreSQL-specific — doesn't work with SQLite (community edition) - Would require maintaining two auth paths: Supabase for Cloud, EnvApiKeyProvider for CE - Supabase Auth has limited organization/tenant support — would need to build tenant management on top - Supabase is a platform dependency — migrating away from Supabase means migrating both database AND auth - The free tier's 50,000 MAU is generous, but the database and auth are tied together — you can't use Supabase Auth without Supabase PostgreSQL - The auth middleware would need separate implementation paths for CE (SQLite + env keys) and Cloud (Supabase + JWT) **Cost Implications:** Free tier. Paid tier starts at ~$25/month (Pro plan includes auth + database). **Risk Assessment:** Medium-High. The tight coupling between Supabase Auth and Supabase PostgreSQL creates platform lock-in. The community edition cannot use Supabase Auth (it runs on SQLite), so maintaining two entirely different auth stacks is inevitable. This is more complex than Option A where both stacks share the same `AuthProvider` interface. --- ## Decision **Chosen option: Option A — Auth0 (External OpenID Connect Provider)** The rationale: 1. **Security delegation is the correct choice for a solo-founder project.** Authentication is the single highest-risk subsystem. Auth0's SOC2-compliant platform handles password hashing (bcrypt, with automatic algorithm upgrades), MFA, breached password detection, brute-force protection, and rate limiting. A solo founder cannot match this with 100h of custom development. 2. **The `AuthProvider` interface makes the dual-provider model clean.** The existing `EnvApiKeyProvider` remains the default for community edition (no config change needed). `Auth0Provider` is a second implementation selected via `AUTH_PROVIDER=auth0` in the environment. The composition root (`src/app/provider-factory.ts`) routes to the correct implementation. Zero changes to middleware or route handlers. 3. **Auth0 Organizations maps directly to DOMINUS tenants.** The `org_id` claim in the JWT is handled by Auth0's native Organizations feature. No custom Actions needed for tenant resolution — it's built into the platform. The middleware extracts `org_id` and injects `req.tenantId`. 4. **CLI API keys still work.** For the Cloud edition, API keys are stored as bcrypt hashes in the database, generated per-user, and scoped to a tenant via `tenant_id`. The auth middleware validates either a JWT (browser/login) or an API key (CLI) using the same `AuthProvider.validate()` interface. 5. **Rejecting Clerk (Option B):** Clerk's React-first model doesn't match DOMINUS's architecture where auth is a backend concern (the SPA is client-side and auth is handled by the Express backend). Custom JWT validation for Express would add complexity without benefit over Auth0's proven `express-oauth2-jwt-bearer`. 6. **Rejecting self-hosted (Option C):** The security risk and development cost are disproportionate for a solo-founder project. Auth0's paid tier (€23/month) is cheaper than the developer time to implement and maintain a production-grade auth system. 7. **Rejecting Supabase (Option D):** The platform lock-in and dual-stack maintenance (Supabase Auth for Cloud, env keys for CE) creates more complexity than the Auth0 approach where both providers share the same interface. ### Architecture Overview ``` ┌─────────────────────────────────────────────────────────────┐ │ Client (SPA / CLI) │ │ Authorization: Bearer │ └──────────────────────┬──────────────────────────────────────┘ │ ┌──────────────────────▼──────────────────────────────────────┐ │ Express Middleware Stack │ │ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ AuthMiddleware (unchanged) │ │ │ │ - Reads Authorization header │ │ │ │ - Calls AuthProvider.validate(token) │ │ │ │ - On success: attaches req.auth = { userId, │ │ │ │ tenantId, role } │ │ │ └────────────────────────┬─────────────────────────────┘ │ │ │ │ │ ┌────────────────────────▼─────────────────────────────┐ │ │ │ AuthProvider.validate() │ │ │ │ │ │ │ │ ┌──────────────────┐ ┌────────────────────────┐ │ │ │ │ │ EnvApiKeyProvider │ │ Auth0Provider │ │ │ │ │ │ (CE) │ │ (Cloud) │ │ │ │ │ │ - static env key │ │ - JWT via JWKS │ │ │ │ │ │ - no tenant │ │ - API key via bcrypt │ │ │ │ │ └──────────────────┘ │ - tenant from org_id │ │ │ │ │ └────────────────────────┘ │ │ │ └──────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` ### JWT Claims Contract | Claim | Source | Description | |-------|--------|-------------| | `sub` | Auth0 user ID | Unique user identifier (`auth0\|`) | | `org_id` | Auth0 Organization | Tenant identifier (UUID v4) | | `role` | Auth0 Org Membership | `admin` or `member` | | `email` | Auth0 user profile | Verified email address | | `iat`/`exp` | Auth0 token | Issued/expiry timestamps | ### API Key Model (Cloud CLI) | Aspect | Decision | |--------|----------| | **Generation** | `dominus auth create-api-key --name "dev"` — shown once, stored as bcrypt | | **Storage** | `api_keys` table: `id, tenant_id, user_id, name, hash_bcrypt, prefix, last_used_at, created_at, revoked_at` | | **Validation** | Auth middleware checks JWT first, falls back to API key (checks bcrypt hash) | | **Prefix** | `dk_dominus_` — displayed in CLI for key identification | | **Revocation** | `dominus auth revoke-api-key ` — sets `revoked_at`, middleware checks it | | **Rotation** | `dominus auth rotate-api-key ` — generates new key, revokes old one | ## Consequences ### Positive - Auth0 handles all identity security (password hashing, MFA, breach detection, rate limiting) - Community edition unchanged — existing `EnvApiKeyProvider` works identically - Clean separation via `AuthProvider` interface — middleware changes are zero - `org_id` claim provides tenant resolution without additional database queries - CLI API keys work in Cloud edition via bcrypt-backed provider - Free tier covers expected launch scale (7,500 MAU) - Path to upgrade: paid Auth0 tier when MAU exceeds free tier - No vendor lock-in at the code level — swapping to Clerk or self-hosted requires only a new `AuthProvider` implementation ### Negative - External dependency on Auth0 for login flow — outage blocks new sessions (not existing ones if JWT is still valid) - Auth0 Dashboard configuration required (tenant, app, API, orgs, Actions for custom claims) - Learning curve for Auth0 Organizations feature and Actions configuration - API key management (bcrypt hashing, database table) adds ~30 lines of middleware logic specifically for Cloud ### Compliance and Security Implications - Auth0 is SOC2, ISO 27001, HIPAA compliant — covers DOMINUS Cloud's compliance requirements - JWKS key rotation is automatic — no manual key management - JWT validation is offline (no outbound API call per request) — no latency or availability dependency for authenticated requests - API keys are hashed with bcrypt (cost factor 12) — never stored in plaintext - Token expiry: access token 15 minutes, refresh token 24 hours, API keys don't expire (manual revocation) - Failed auth attempts are rate-limited at the Auth0 level AND the Express middleware level (defence-in-depth) ### Migration and Monitoring Plan 1. Implement `Auth0Provider` in `src/providers/auth/auth0-provider.ts` with JWKS validation 2. Add `AUTH_PROVIDER`, `AUTH0_DOMAIN`, `AUTH0_AUDIENCE`, `AUTH0_JWKS_URI` to config schema 3. Add `api_keys` table migration for Cloud CLI API keys 4. Wire provider selection in `src/app/provider-factory.ts` 5. Test: JWT validation, API key validation, tenant extraction, role extraction, expired token rejection 6. Monitor: auth failure rate, token validation latency, JWKS fetch frequency 7. Rollback: set `AUTH_PROVIDER=env` — all Cloud users lose access, CE users unaffected ### Validation - Integration tests verify that a valid Auth0 JWT passes `Auth0Provider.validate()` and returns correct `{ userId, tenantId, role }` - Integration tests verify that expired, malformed, and wrong-audience JWTs are rejected - Integration tests verify that community edition with `AUTH_PROVIDER=env` works identically to v0.5.0 - Integration tests verify that rate limiting on auth failures works for both providers - API key integration tests verify bcrypt validation, revocation, and rotation --- *This ADR was created following the MADR 4.0.0 standard. All DOMINUS ADRs should be consistent with the ADR series starting at `docs/adr/0001-project-architecture.md`. Template: `.claude/skills/adr/template.md`.*