Security Fix: Closing the tRPC Middleware Authentication Gap
Security Fix: Closing the tRPC Middleware Authentication Gap
Release v1.0.436 · SOC2-03 Compliance
Background
This platform enforces authentication at multiple layers. For most routes, src/middleware.ts acts as the outermost gate — redirecting unauthenticated requests before they reach application logic. Inside tRPC, individual procedures are further protected by declaring them as protectedProcedure, which checks for a valid session before executing any handler.
These two layers are designed to be complementary. The middleware provides a catch-all safety net; protectedProcedure provides per-procedure enforcement.
The Gap
Prior to this release, /api/trpc was listed explicitly in the isPublicRoute array inside src/middleware.ts:
// src/middleware.ts (before fix)
const isPublicRoute = createRouteMatcher([
'/login',
'/api/auth(.*)',
'/api/trpc', // ← this bypassed the outermost auth layer
]);
This meant the middleware unconditionally allowed all requests to every tRPC endpoint through without evaluating session state. Authentication was deferred entirely to the tRPC layer.
Why this matters
Relying solely on protectedProcedure is workable in practice, but it creates a fragile single point of failure. If a developer defines a procedure as publicProcedure — even by mistake, or during rapid prototyping — that endpoint is immediately reachable by anyone on the internet with no middleware interception.
In a platform handling HMRC OAuth tokens, encrypted bank credentials, and taxpayer financial data, that risk is unacceptable under SOC2-03 controls.
The Fix
/api/trpc has been removed from the isPublicRoute list. The middleware no longer pre-emptively marks tRPC requests as public.
// src/middleware.ts (after fix)
const isPublicRoute = createRouteMatcher([
'/login',
'/api/auth(.*)',
// /api/trpc is no longer listed here
]);
Critically, this does not break tRPC functionality. The Auth.js middleware wrapper still processes every request and attaches the session to req.auth. Authenticated users experience no change. Unauthenticated requests to protectedProcedure endpoints will now be caught at both the middleware layer and the tRPC layer.
Defence in Depth
With this change, the authentication architecture now operates as intended:
| Layer | Mechanism | Catches |
|---|---|---|
Middleware (src/middleware.ts) | Auth.js session check | All unauthenticated requests to /api/trpc |
| tRPC procedure | protectedProcedure | Unauthenticated calls that reach procedure handlers |
Neither layer alone is sufficient. Together they ensure that a misconfigured procedure cannot silently become a public endpoint.
Recommended Follow-up: CI Lint Rule
As an additional safeguard, consider adding a lint rule to your CI pipeline that flags any tRPC procedure using publicProcedure that also accesses ctx.session, ctx.user, or any user-scoped data source. This would surface accidental misconfigurations at pull-request time, before they reach a deployed environment.
Example check logic (pseudocode):
For each tRPC router file:
If procedure is declared with publicProcedure:
If handler body references ctx.session OR ctx.user:
Fail with: "publicProcedure must not access user-scoped context — use protectedProcedure"
SOC2-03 Control
This change directly addresses the SOC2-03: Authentication — Route Protection control, which requires that all routes handling authenticated or user-scoped data are protected at the outermost applicable layer, with no reliance on a single enforcement point.