Implement authorization using context getters, context schemas, and composable functions. Use when working with authentication, permissions, user access control, protected routes, business logic authorization, or when user mentions auth, context, permissions, or access control.
Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.
Quelldateien prüfen
Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.
Mit Codex oder Claude installieren Kopieren Sie diesen Prompt, fügen Sie ihn in Codex, Claude oder einen anderen Assistant ein und lassen Sie die Skill-Seite prüfen und installieren.
Ein direkter Befehl überspringt den Prüf-Prompt. Prüfen Sie die Quelle, bevor Sie ihn ausführen.
Implement authorization using context getters, context schemas, and composable functions. Use when working with authentication, permissions, user access control, protected routes, business logic authorization, or when user mentions auth, context, permissions, or access control.
Authorization
This skill documents the authorization architecture used in this repo. The pattern enforces security through three coordinated layers: UI components, route loaders/actions, and business functions.
Overview
Authorization uses a layered approach where each layer has specific responsibilities:
Components - Render UI based on loader data, no authorization logic
Loaders and Actions - Obtain context via getters, perform redirects, pass context to business functions
Business Functions - Enforce authorization with context schemas, throw errors on violations
This architecture ensures that:
Authorization logic is centralized and consistent
Every layer validates permissions appropriately
Business functions cannot be invoked without proper context
Unauthorized access is caught early and redirected appropriately
Three-Layer Architecture
Layer 1: Components
Components rely on data returned from loaders to make UI decisions. They determine whether to render privileged buttons, links, or sections based on the data available.
Key principle: Components do not perform redirects or authorization checks. They trust loader data.
<mainclassName="container mx-auto p-4 pt-16"><title>Dashboard</title><divclassName="flex items-center justify-between"><h1className="h1">Dashboard</h1><Formmethod="post"action="/auth/sign-out"><buttontype="submit"className="btn btn-primary">
Sign Out
</button></Form></div></main>
In this example, the component simply renders UI. The getUserContext call in the loader ensures only authenticated users reach this component (unauthenticated users are redirected).
Layer 2: Loaders and Actions
Loaders and actions obtain environment and user information via context-getter utilities. These helpers perform authorization checks and redirect when users lack permission.
Available context getters:
getContext - Returns base context (may include currentUser: null)
getUserContext - Requires authenticated user, redirects to /auth if not
getCompanyContext - Requires authenticated user with an active company scope
getAdminContext - Requires authenticated user with admin privileges
Key principle: When invoking business functions, use the act or load helpers to ensure context is always passed along.
Example with getUserContext (app/business/auth.server.tsx):
Further extend for domain-specific contexts (e.g., companyContextSchema, adminContextSchema)
Each schema should validate exactly what the business function needs to operate safely.
Role Abstractions
When a role or authorization concept needs to be shared across multiple places, make context-getters return an abstracted value and reference it in schemas.
Example (app/business/auth.server.tsx):
asyncfunctiongetCompanyContext(request: Request, params: Params) {
const { currentCompany, currentMembership, ...context } =
awaitgetUserContext(request, params)
if (!currentCompany) {
throwredirect('/', {
headers: awaitsetFlashMessage(request)(
'You do not have access to this page.'
),
})
}
return {
...context,
currentCompany,
currentMembership: currentMembership,
}
}
const companyContextSchema = userContextSchema.extend({
currentCompany: currentCompanySchema,
})
The abstracted currentCompany and currentMembership can then be referenced in:
Context schemas for validation
Business functions for company-scoped access checks
Helper functions like isCompanyManager(currentMembership)
Best practice: Keep role abstractions small and broadly useful. Avoid proliferating too many abstractions.
Using act() and load() Helpers
When invoking business functions from loaders or actions, use the act() or load() helpers to ensure context is passed correctly.
When context schemas aren't enough, add explicit checks in business functions and throw errors:
const discardProject = applySchema(
z.object({ projectId: z.string() }),
userContextSchema
)(async ({ projectId }, context) => {
const project = awaitdb()
.selectFrom('projects')
.where('id', '=', projectId)
.select(['ownerId'])
.executeTakeFirst()
if (!project) {
thrownewError('Project not found')
}
if (project.ownerId !== context.currentUser.id) {
thrownewError('Only the project owner can discard it')
}
// Proceed with appending the discard event...
})
Prefer schemas when possible, but don't hesitate to add custom checks for complex authorization logic.
Every foreign id in a write is a tenancy check
Context schemas prove who the caller is; they say nothing about what the input ids point at. In a multi-company schema, every foreign entity id accepted as input — on creates as much as edits — must be verified against the current company before use (belongsToCompany or an ownership-scoped fetch). A function that checks locationId but inserts productId verbatim is a cross-tenant leak through the unchecked id, and a nonexistent id surfaces as a raw FK 500 instead of a friendly InputError.
The same applies to user-suppliable overrides of server-derived values (a typed price, a manual quantity): the override changes the amount only — the underlying entity still gets full tenancy and existence validation, and every such path gets a cross-company negative test.
Authorize action-only routes before any keyed read
Action-only routes must authorize explicitly at the top of the action, before ANY params-keyed database read or input-schema selection. Context getters return capability booleans without throwing, and applySchema's context validation runs only inside act() — so anything computed before act() runs for every authenticated user. A params-keyed lookup, or a branch that picks between input schemas before the authorization check, leaks information through differential behavior: a validation error that differs by the target's state tells any signed-in tenant something about a resource they cannot access.
Match the router's path normalization in a prefix gate
A gate that authorizes by URL prefix must resolve the request path exactly as the router resolves it, or a cosmetic URL variant the router still routes to the surface bypasses it. React Router matches case-insensitively and percent-decodes each path segment before matching, so a raw, case-sensitive prefix comparison lets /app/Work-Orders and /app/work%2Dorders reach the real loader and action while the gate sees no match — a read and write bypass at once. Normalize before comparing: split on /, decodeURIComponent each segment (falling back to the raw segment when decoding throws), then toLowerCase. Pin adversarial-path tests on every such gate — mixed case, percent-encoded, and trailing slash — and for a write surface assert the action returns 404 and that no row was written before the write.
Testing permission gates
Never test an authorization gate with a context that grants either nothing or everything — such a test stays green when the getter checks the wrong key (an approve_order/approve_invoice swap passes both ways). Grant exactly the one permission key under test through the real fixture and the real context getter, assert the sibling capabilities stay false, and prove the test by swapping the key literal in the getter and watching that specific test fail.
Best Practices
Always use context getters in loaders/actions - Never manually check authentication; let getters handle it
Always use context schemas in business functions - Validate context at the function boundary
Keep authorization centralized - Don't scatter auth checks across many files
Use appropriate context level - getContext for public routes, getUserContext for authenticated routes, getCompanyContext for company-scoped routes, getAdminContext for admin-only routes
Trust the layers - Components trust loaders, loaders trust context getters, business functions trust schemas
Add custom checks sparingly - Prefer schema validation; add explicit checks only when schemas can't express the requirement
Abstract wisely - Create role abstractions when concepts are shared broadly, but avoid over-abstracting
Resource Routes
Resource routes serve XHR/fetch clients instead of rendering pages — file endpoints, data endpoints, webhooks — and are typically mounted at the top level of routes.ts, outside every layout. The three-layer architecture applies to them with two adjustments:
They guard themselves. A route outside every layout inherits nothing from layout loaders; the module's own loader/action is the only gate. Call a context getter as the first statement, before reading the request body, and never assume the surrounding app implies a session.
Match the response to the client's shape. A resource route consumed programmatically (XHR/fetch — /upload is the canonical case) answers with status codes: use getContext and return 401 (no session), 403 (no permission), or the appropriate 4xx, because a login redirect surfaces there as a garbled parse failure. A resource route consumed by real browser navigations (<a>, <a download> — /download is the canonical case) uses getUserContext: its sign-in redirect carries a return-to that resumes the navigation after login, which no status code can do. When one route serves both shapes (/download is also the src of <img> tags, where a redirect renders as a silently broken image), design for the deliberate interaction — the click — and accept the degraded secondary.
Client-side validation on the calling UI (file-size caps, type restrictions) is advisory UX only. Enforce every limit again in the resource route: an attacker talks to the endpoint directly, not to the component in front of it.
exportasyncfunctionaction({ request, params }: Route.ActionArgs) {
const { currentUser } = awaitgetContext(request, params)
if (!currentUser) {
returnnewResponse('Please sign in to continue.', { status: 401 })
}
// Permission check → 403, content-type/size limits → 4xx, then the work
}
Troubleshooting
"Context schema validation failed"
Cause: Business function received context that doesn't match the required schema.
Solution: Ensure the loader/action uses the appropriate context getter:
For contextSchema → use getContext
For userContextSchema → use getUserContext
For companyContextSchema → use getCompanyContext
For adminContextSchema → use getAdminContext
Redirect loops
Cause: Protected route redirects to auth, which redirects back, infinitely.
Solution: Check that auth routes use getContext (not getUserContext). Ensure getOptionalCurrentUser is used correctly in context getters.
TypeScript errors on context properties
Cause: Context schema doesn't match the actual context type.
Solution: Ensure context getter returns all properties defined in the schema. Check that schema extends are correct (e.g., userContextSchema extends contextSchema).
Business function doesn't redirect unauthorized users
Cause: Business function validates context but doesn't get invoked via loader/action context getter.
Solution: Always call context getters in loaders/actions before invoking business functions. The getter handles redirects; business functions handle validation and errors.
Project-empirical lessons about this skill land in workflow-content/authorization.md through a pull request on the project — never by editing this file, which is regenerated on every upgrade. A lesson that turns out to be true of every project travels as an issue on the workflow package instead.