Skip to content

ADR-0102: AuthN/AuthZ architecture for app.tlsstress.art

  • Status: Accepted
  • Date: 2026-05-28
  • Driver: Project owner request — full rebuild of sign-up + sign-in following modern B2B SaaS standards (NIST SP 800-63B, LGPD, GDPR, ISO 27001, SOC 2, OWASP ASVS)
  • Supersedes: ADR-0099 §"Customer auth foundation" (HMAC-only)
  • Sources: docs/auth-blueprint/Arquitetura_Seguranca_SaaS_B2B_v2.md (operator blueprint v2, 2026-05-28)

Context

The current customer-app auth (/api/auth/signup + /api/auth/login + HMAC customer_session cookie) was built for go-live speed. It works but does not meet the operator-mandated security blueprint for a globally-marketed B2B SaaS subscription product.

Blueprint v2 (revised 2026-05-28) imposes 6 architectural decisions that shape the entire auth surface. This ADR locks those decisions before implementation begins.

Decision

D1 — Framework: Next.js + NestJS-like module structure (no framework switch)

Keep Next.js App Router (current prod). Adopt NestJS-inspired pasta organization under src/lib/:

src/lib/
  auth/          # authn (signup, signin, refresh, logout)
  mfa/           # totp + backup codes + step-up
  recovery/      # password reset flow
  email-validation/  # pipeline: normalize → blocklist → MX → opt-in
  authorization/ # RBAC + ABAC per-tenant
  risk/          # geo + velocity + fingerprint + IP reputation
  audit/         # hash-chained events + SIEM export
  sessions/      # JWT access + refresh rotation

Rationale: the blueprint requires "segregation between authN/authZ/audit as distinct services with separate storage". The Next.js route handlers become thin controllers that delegate to module services — same spiritual segregation without a 2-3 week framework migration.

D2 — Edge: Cloudflare proxy ON (reactivate)

Reactivate Cloudflare orange-cloud proxy on app.tlsstress.art and admin.tlsstress.art. Replace the AWS ACM cert on App Runner with a Cloudflare Origin Certificate (15-year validity, free).

This enables: - Cloudflare WAF with OWASP Core Rule Set - Cloudflare Bot Management (deeper than Turnstile alone) - Geo-fencing at the edge (block embargoed countries before app) - DDoS L7 + Argo Smart Routing

Carries risk: reactivation may regress Stripe webhook signature (CF adds headers) and Turnstile (already CF-native — should be fine). Smoke both immediately after flip.

D3 — Compute: AWS App Runner (cloud) + k3s (on-prem)

No migration to EKS for the cloud SaaS — App Runner is 6× cheaper than EKS at our scale and the operator confirmed it stays. The blueprint's "Infra: Kubernetes" maps to: - Cloud customer-app: AWS App Runner (this stays) - On-prem modules: k3s (already shipped per ADR-0101)

If/when we cross the 3-5 microservice threshold, revisit EKS migration.

D4 — Observability: OpenSearch AWS (managed) as SIEM sink

Adopt @opentelemetry/sdk-node with auto-instrumentation. Export traces + logs + metrics to Amazon OpenSearch Service (managed). No third-party vendor (Datadog, New Relic).

This unifies: - App Runner application traces - Database query spans (Drizzle pg adapter) - Auth event correlation (every signup, signin, MFA, refresh tagged with correlationId) - SIEM dashboards for the operator (Open Distro Security / Kibana)

Free tier: AWS gives 1 t3.small.search node free for 12 months; afterwards ~$25/mo for a single-node domain in us-east-1.

D5 — Email blocklist: DB-versioned with admin UI

Replace any hardcoded blocklist with a email_blocklist table:

CREATE TABLE email_blocklist (
  domain          TEXT PRIMARY KEY,           -- lowercased apex
  category        TEXT NOT NULL,              -- 'free' | 'disposable' | 'competitor' | 'custom'
  rationale       TEXT,                       -- why blocked (audit-friendly)
  added_by        UUID REFERENCES customer_users(id),
  added_at        TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  removed_at      TIMESTAMPTZ,
  removed_by      UUID REFERENCES customer_users(id),
  list_version    INT NOT NULL,               -- bump per batch update
  -- For audit + compliance proof
  source          TEXT,                       -- 'disposable-email-domains-npm' | 'manual' | etc.
  source_version  TEXT                        -- npm pkg version or commit sha
);
CREATE INDEX email_blocklist_active_idx
  ON email_blocklist(category) WHERE removed_at IS NULL;

Seed at apply-time with: - disposable-email-domains (npm pkg) — ~3k disposable domains - Free providers: gmail.com, outlook.com, hotmail.com, yahoo.com, icloud.com, proton.me, aol.com, live.com, msn.com, mail.com, gmx.com, yandex.com, 163.com, qq.com, naver.com, seznam.cz, mail.ru, web.de - Competitors (manually curated): keysight.com, spirentcom.com, viavisolutions.com, ostinato.org

Admin UI at /admin/email-blocklist for managing entries + audit trail of additions/removals.

D6 — Sessions: JWT access (5-15 min) + refresh JWT rotation (httpOnly)

Refactor from HMAC cookie to full JWT model:

  • Access token: JWT signed with LICENSE_JWT_SECRET-style HS256 (or new AUTH_JWT_SECRET), 15-minute TTL, sent as Authorization: Bearer <jwt> OR tlsstress_at httpOnly cookie.
  • Refresh token: JWT signed with separate secret, 30-day TTL, stored as tlsstress_rt httpOnly cookie with SameSite=Strict; Secure; Path=/api/auth/refresh.
  • Refresh family tracking: each refresh token carries a family_id
  • parent_jti. On /api/v1/auth/refresh the old token is marked used_at; presenting an already-used refresh = token theft → revoke the entire family + audit event + force re-login.
  • Schema: customer_refresh_tokens table (jti, family_id, parent_jti, user_id, account_id, issued_at, expires_at, used_at, revoked_at, ip, ua).

This is a larger refactor than option A but aligns with the blueprint's "refresh token reused → revoke entire family + alert" requirement.

Consequences

Positive

  • Aligns 100% with operator-mandated blueprint
  • Refresh rotation gives revoke-at-leak-detection capability
  • DB-versioned blocklist is auditable for compliance reviews
  • OpenSearch SIEM gives correlation IDs across the request lifecycle
  • Cloudflare WAF reduces application-layer load (rate limit / bot drop at edge)

Negative / Risks

  • JWT refactor breaks all existing customer sessions — operator must notify customers (single email blast) before deploy. Sessions reset to login.
  • Cloudflare proxy reactivation risks Stripe webhook signature verification (CF strips/rewrites some headers); needs smoke test in Test mode before flipping production.
  • OpenSearch managed adds $25/mo recurring cost after free tier (acceptable for compliance value).
  • Email blocklist as policy may reject some legitimate B2B users at free providers (the blueprint v2 explicitly accepts this trade-off for lead qualification).

Implementation roadmap (6 waves, ~3 weeks)

Wave Scope Estimate
W1 Email pipeline (blocklist + MX + HIBP) + Argon2id pepper + signup refactor 3-4 days
W2 2FA TOTP enrollment-mandatory + backup codes + step-up 3-4 days
W3 Sign-in flow refactor (JWT access + refresh rotation + lockout progressive) 3-4 days
W4 Recovery flow (uniform response + HIBP on new pwd + session invalidation) 2 days
W5 Risk engine + CF WAF reactivation + audit hash-chained 4-5 days
W6 OpenTelemetry → OpenSearch + customer RBAC/ABAC wire-up 3-4 days

How to verify

After all 6 waves:

# 1. Sign-up flow
curl -X POST https://app.tlsstress.art/api/v1/auth/signup \
  -d '{"email":"test@gmail.com",...}' # → 400 free_domain_blocked
curl -X POST .../api/v1/auth/signup \
  -d '{"email":"test@acme.com",...}' # → 201 + verify email sent

# 2. Sign-in flow (verify timing-uniform)
time curl -X POST .../api/v1/auth/signin -d '{"email":"nobody@x.com",...}'
time curl -X POST .../api/v1/auth/signin -d '{"email":"real@user.com","password":"wrong"}'
# Both should take same time (~argon2id verify cost)

# 3. MFA enrollment mandatory
# After signup+verify, /account/* returns 403 until /api/v1/auth/mfa/enroll

# 4. Refresh rotation
# POST /api/v1/auth/refresh with old token after refresh = 403 + family revoked

# 5. Edge WAF
curl -A "sqlmap" https://app.tlsstress.art/ # → 403 blocked at CF

# 6. SIEM correlation
# Every endpoint should emit OpenTelemetry span with correlationId
# OpenSearch dashboard at /admin/security shows the trail

Pointers

  • Memory: [[../memory/project_auth_rebuild_b2b_blueprint_2026_05_28]]
  • Prior: [[0099-token-economy-v5]] — tier definitions consumed by AuthZ
  • Prior: [[0100-token-economy-security-and-admin]] — admin auth (this ADR is customer-side parallel)
  • Prior: [[0101-saas-onprem-loop]] — license JWT model (similar architecture, different audience)
  • Blueprint source: operator-supplied Arquitetura_Seguranca_SaaS_B2B_v2.md (private)