Skip to main content

AUTH_RISK_MATRIX.md — DemozPay

Companion to AUTH_SYSTEM_REVIEW.md. Scoring: Likelihood (L) and Impact (I) each 1–5; Risk = L × I. Priority bands: 🔴 ≥15 · 🟠 9–14 · 🟡 4–8 · 🟢 ≤3. "Stage to fix" maps to the 30/90/180 roadmap in the review.


A. Current-system risk register (what exists today)

#RiskClassLIScoreBandStage to fix
R1S2S gRPC createInsecure() — API↔ledger↔gateway unauthenticated + unencrypted (money tier)Security / Compliance4520🔴30d
R2Platform-admin: cross-tenant superuser, no MFA / step-up / separate realmSecurity / Insider3515🔴30–90d
R3No rate limiting / lockout on password + OTP (credential stuffing, OTP brute-force)Security4416🔴30d
R4Auth subtree bypasses Nest pipeline — no shared rate-limit/validation/trace on login/OTP/resetSecurity / Operational4416🔴30d
R5Phone OTP not production-ready (LoggingSmsSender drops codes in prod) — primary market pathOperational / Security4312🟠30–90d
R6No immutable auth event log (logins, MFA, role grants, admin actions, impersonation)Compliance / Audit3412🟠90d
R7Webhook nonce signed but not enforced → replay within 5-min skew windowSecurity248🟡90d
R8MFA absent platform-wide (TwoFactor table, plugin not wired)Security3412🟠90d
R9Per-request getSession() + per-RBAC 2 DB queries, no cacheScaling / Operational339🟠90–180d
R10Per-route opt-in RBAC — undecorated money route = any authenticated userSecurity248🟡90d
R11Dual role model (Member.role vs legacy Role+permissions)Maintenance / Tech-debt326🟡90–180d
R12Middleware coverage = manual 4-controller allow-list — new controllers silently uncoveredMaintenance / Operational339🟠90d
R13Schema/session-shape lock-in to Better Auth (no isolation abstraction yet)Vendor / Maintenance339🟠30–90d (abstraction)
R14Opaque DB sessions don't federate across regionsScaling236🟡180d+ (decision gate)
R15Weak password policy (min-8 only, no breach/complexity check)Security224🟡90d
R16Better Auth version churn (young lib, plugin API breaking changes)Ecosystem326🟡Ongoing (regression suite)
R17Legacy shared/auth JWT primitives still shippedTech-debt212🟢180d

Top 4 to fix first (the only 🔴s): R1, R3, R4, R2. None of them are "replace Better Auth". All four exist regardless of the library.


B. Option-comparison risk (the decision itself)

Risk introduced by choosing each path, independent of current gaps.

DimensionA — Keep + hardenB — ForkC — CustomD — Enterprise IAM
Security-regression risk🟢 Low🟡 Med🔴 High🟡 Med (migration)
Delivery risk (will it ship)🟢 Low🟠 Med-High🔴 Very high🟠 Med-High
Long-term maintenance🟡 Med🔴 High (own a fork forever)🔴 Very high🟢 Low (vendor/community)
Compliance/auditor story🟡 Med🟡 Med🔴 Weak ("rolled our own")🟢 Strong (attested IAM)
Lock-in🟡 Med (mitigated by abstraction)🟠 High (your fork)🟢 None (you own it)🟠 Vendor-dependent (Auth0 high, Ory/Keycloak low)
Cost to reach parity🟢 ~0🟠 Med🔴 6–9 mo🟠 2–4 mo migration
Regulator perception🟡 Neutral🟡 Neutral🔴 Negative🟢 Positive
Partner-bank perception🟡 Neutral→Positive after hardening🟡 Neutral🔴 Negative🟢 Positive

Lowest-risk path: A now → D at a triggered scale-up gate. B and C carry risk no current requirement justifies.


C. Attack-surface map (auth-relevant)

SurfaceEntry pointCurrent controlResidual riskRef
Credential loginPOST /api/auth/sign-inpassword hash (Better Auth), CSRF via trustedOriginsNo rate limit / lockout (R3); outside Nest pipeline (R4)better-auth.factory.ts:42
OTP loginPOST /api/auth/phone-number/*OTP 5-min expiryNo rate limit (R3); prod sender drops codes (R5)sms-sender.ts
Password resetBetter Auth reset flowtoken expiry (lib)No rate limit; not exercised/verified herefactory
Session usecookie → every /api/*opaque DB token, fail-closed guardDB hit/req (R9)session.middleware.ts
Tenant scopingreq.user.businessId → ALS → RLSRLS-forced financial tables, headers rejectedresidual = undecorated route (R10)tenant-context.middleware.ts
Privilege escalation@RequireOrgRole / platform-adminMember/AdminProfile checks, single role sourceadmin path = unconditional bypass, no MFA (R2)org-role.guard.ts:94
Partner webhookPOST /api/integration/bank-callback/:partnerHMAC + skew + timing-safereplay in window (R7)bank-webhook.controller.ts
Service-to-servicegRPC → ledger / gatewaynone🔴 unauthenticated, cleartext (R1)*.grpc-client.ts
Admin operationsplatform-admin endpointsRBAC bypassno MFA, no impersonation audit (R2, R6)org-role.guard.ts

D. Migration risk matrix (if/when moving off Better Auth → see AUTH_MIGRATION_STRATEGY.md)

Migration riskLIScoreMitigation
Session invalidation forcing mass re-login428Dual-read sessions during cutover; expire-don't-revoke
Password hash format incompatibility3412Lazy rehash-on-login; keep Account.password readable
Org/member mapping drift248Keep Organization.id == Business.id invariant; reconciliation job
RBAC semantics change339Abstraction layer freezes role contract before migration
Tenant-context regression (RLS)2510RLS stays at DB regardless of IDP — do not couple to migration
Audit-trail continuity break339Auth-event log lives outside IDP from day one
OIDC/federation misconfig3412Adopt vetted OP; never hand-roll RP validation

Principle: the abstraction layer (§6 of the review) is what converts every cell above from "rewrite risk" to "config + reconciliation risk." Build it before you need it.