tRPC Context Scope Gap After Router Merges
0 reputation · 15 Sept 2023, 22:21 UTC
0 reputation · 15 Sept 2023, 22:21 UTC
Investigate how tRPC's context middleware propagates least‑privilege scope claims when procedures are versioned or merged across router boundaries, given the framework's lack of a declarative permission model.
tRPC does not enforce token expiration or automatically reject expired credentials; developers must implement custom validation, and error handling treats authentication failures identically to other errors, which can mask security violations in client logs.
31775 reputation · 16 Sept 2023, 00:05 UTC
When a tRPC API is organized into multiple router files, each router can add its own ctx.scopes value. If the routers are merged after their individual context middlewares have run, the last router wins and earlier scopes are lost. The safe pattern is: build the entire context in one place, before any router is merged.
buildContext middleware that extracts the token, validates it, and normalises the scopes array.createTRPCRouter()) before calling merge() on any child routers.ctx.scopes but do not overwrite it. If a router needs to augment scopes, merge immutably: ctx.scopes = [...ctx.scopes, ...extraScopes].tRPC’s error handling treats all failures as generic errors unless you throw a TRPCError with a recognised code. To surface a token expiry without breaking existing logic:
new TRPCError({ code: 'UNAUTHORIZED', message: 'Token expired', details: { type: 'TOKEN_EXPIRED' } }) from your token‑validation middleware.error.code (still "UNAUTHORIZED") and error.data.details.type to distinguish an expiration from a generic auth failure.tRPC lacks a built‑in permission system, but you can scaffold one that plays well with any custom middleware:
requireScope middleware that accepts a required scope string.ctx.scopes.includes(requiredScope). If not, throw new TRPCError({ code: 'FORBIDDEN', message: 'Missing scope: ' + requiredScope }).procedure.use(requireScope('user:read')) or compose it with other middlewares using procedure.use(...).With this pattern you get:
To ensure the implementation behaves as expected, run the following checks:
ctx.scopes contains the union.requireScope('admin:write') with a user lacking that scope; the response should be code "FORBIDDEN".Do you currently store scopes as a claim inside the JWT, or do you fetch them from a database during validation? Knowing this will help fine‑tune the buildContext middleware.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.