Skip to content

[v3 auth] Add authoritative request-scoped server permission enforcement #177

Description

@olliethedev

Parent

Depends on

Related

Purpose

Make the server half of the v3 can() contract directly usable and authoritative. This is the second delivery stage of #173.

Current DX

StackServerAuthProvider exposes can, but the RC2 request path registers and invokes only getIdentity. An application can pass the same policy to client and server configuration and still has to recreate separate session/role checks in every lifecycle hook:

stack({
  auth: { getIdentity: getRequestIdentity, can: canUseStack },
});

blogBackendPlugin({
  onBeforeDeletePost: async (_id, context) => {
    const session = await getSession(context.headers);
    if (session?.user.role !== "admin") throw new Error("Unauthorized");
  },
});

This creates false confidence, repeated session work, policy drift, and generic errors that do not distinguish unauthenticated from forbidden requests.

Proposed DX

Expose framework-neutral request-scoped primitives that resolve the memoized request identity and invoke the configured authoritative policy:

await requireRequestPermission(context.headers, {
  resource: "blog:post",
  action: "delete",
  params: { id: postId },
});

The server is the source of truth. A browser-safe pure evaluator may also be used by the client, but executing the same function in both environments is not required. Database-, tenant-, ownership-, or secret-dependent policies remain server-only.

Scope

  • Register the server auth provider and request alongside the existing memoized request identity resolver.
  • Add public canRequest and requireRequestPermission primitives using the registered permission type from [v3 auth] Define the typed permission vocabulary and extension registry #176.
  • Invoke StackServerAuthProvider.can with the memoized identity, request headers, and server-derived resource/action/params request.
  • Define behavior when auth is absent, can is absent, identity resolution fails, the policy denies, or the policy throws.
  • Define consistent unauthenticated, forbidden, and internal-policy-error outcomes through a BTST HTTP error contract.
  • Keep the implementation based on standard Request, Response, and Headers rather than framework-specific request or redirect helpers.
  • Ensure record ids and security-relevant parameters are reconstructed from the route/body/loaded record rather than trusted from a client authorization claim.
  • Document lifecycle hooks adding ownership or domain rules after the base policy rather than duplicating it.
  • Prove that client gating cannot authorize a backend request and that configuring server can does not silently protect an endpoint that has not declared permission semantics.

Error semantics to define

  • No identity plus a policy denial must produce the documented unauthenticated result.
  • A resolved identity plus a policy denial must produce the documented forbidden result.
  • A policy exception must fail closed without being misreported as an authorization success; its HTTP/logging behavior must be explicit.
  • Current permissive behavior when auth or can is omitted must either be preserved and documented or changed only through an explicit v3 breaking decision.

Acceptance criteria

  • canRequest evaluates the configured server policy with the memoized identity and registered permission tuple.
  • requireRequestPermission returns normally on allow and produces the documented typed HTTP error on deny.
  • Multiple checks during one request resolve identity once.
  • The policy receives server-derived record/context parameters with the types introduced by [v3 auth] Define the typed permission vocabulary and extension registry #176.
  • Anonymous denial, authenticated denial, authenticated allow, identity failure, policy exception, absent auth, and absent can have explicit tests.
  • 401/403/internal-error mapping is consistent and does not rely on generic Error("Unauthorized") strings.
  • The helper works through standard Request/Headers contracts without imports from Next.js, React Router, or TanStack Start.
  • A negative test proves a client-side allow result cannot bypass the server policy.
  • A negative test proves an undeclared endpoint is not claimed to be protected automatically.
  • Existing lifecycle hooks remain supported and can add record-aware rules after the centralized policy check.
  • Request-scoped state is garbage-collectable and is not treated as persistent user state across requests, isolates, or serverless invocations.
  • Package typecheck, server auth unit tests, representative lifecycle-hook tests, and docs build pass.

Non-goals

  • Automatically guessing permissions for built-in or custom endpoints.
  • Migrating every first-party control and endpoint; that is the third stage under [v3 auth] Make can() a typed full-stack authorization contract #173.
  • Running a privileged server policy in the browser.
  • Building a generalized remote-capability client, authorization DSL, or policy database.

Blocks

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions