Course outline

GraphQL / Intermediate: policy, sources and clients / Lesson 7

Resolvers, request context and authorization

Adapt domain services through resolvers while keeping identity and object policy consistent across graph paths.

About 4 min readContent reviewed
On this page

Lesson goals

  • Separate parent values, arguments, context and field metadata.
  • Apply shared tenant and object policy rather than trusting graph entry points.

Keep resolvers narrow and domain rules shared

A resolver adapts one schema field to an application capability. In common JavaScript implementations it receives a parent value, coerced arguments, request context and field information. Context can hold the authenticated principal and request-scoped data access, but it should not become a global mutable bag shared across callers. Put reusable business decisions in a service or policy that other application entry points also use. Otherwise a course reached through Query.course may be protected while the same course reached through author.courses bypasses the rule.

Require an explicitly scoped service

The fragment below passes the caller’s tenant and principal to a domain service rather than loading a raw global record by ID. It is not a complete server: authentication middleware, schema and service implementation are intentionally outside the snippet. The service must define whether an inaccessible object becomes null or an approved error without leaking its existence. Keep the resolver’s returned object compatible with the public schema and avoid attaching secret fields merely because the internal entity contains them.

javascript Resolver-to-policy boundary
const resolvers = {
  Query: {
    course: (_parent, { id }, context) =>
      context.courses.findVisible({
        id, tenantId: context.principal.tenantId,
        principal: context.principal
      })
  }
};

The resolver delegates visibility to findVisible. The fragment assumes an authenticated principal and does not implement that authentication or data access.

Authorization must survive alternate paths

A graph naturally exposes multiple routes to one entity. Root lookup checks alone are therefore insufficient. Review list membership, node lookup, nested relationships and mutation payloads for the same eligibility rule. Field-level restrictions may also differ: a public title and a private enrollment count need separate consideration even on the same Course object. Authentication answers who the caller is; authorization decides which action that caller may perform now. Never infer access from a client-supplied tenant argument or from an opaque ID that happens to be difficult to guess.

Exercise tenant isolation as a local predicate

This complete offline function models a deliberately small visibility rule: a published course is readable only inside its tenant. It is useful for checking the decision table, but it is not a production access-control system or a GraphQL executor. Real policies may include roles, enrollment, legal retention and revocation. The important pattern is to test both a positive and a denied case with the same record identity. A same-tenant draft also remains denied, demonstrating that tenancy alone does not imply every permission.

javascript Offline visibility decision table
function canRead(principal, course) {
  return principal.tenantId === course.tenantId &&
    course.state === "PUBLISHED";
}
const course = { id: "c1", tenantId: "lab-a", state: "PUBLISHED" };
console.assert(canRead({tenantId: "lab-a"}, course));
console.assert(!canRead({tenantId: "lab-b"}, course));
console.assert(!canRead({tenantId: "lab-a"}, {...course, state: "DRAFT"}));

All three assertions pass for this declared rule. They establish only the predicate behavior, not transport identity or full application security.

Bound context lifetime and error disclosure

Create request-scoped caches and loaders after identity is established, and discard them at the end of the relevant operation lifecycle. A process-global loader can accidentally return one tenant’s cached entity to another request. Long-lived subscriptions need an explicit identity recheck and resource cleanup policy rather than assuming the initial context remains eligible forever. Public errors should communicate a stable safe category while internal traces preserve a controlled correlation identifier. Test the policy without a live service, then verify real middleware integration only in an independently authorized environment.

Common errors and fixes

Nested relationships expose a hidden course

Why it happens
Only the root resolver applied visibility.
How to fix it
Use the same domain policy for every traversal and payload.
Prevent it next time
Test alternate graph paths for the same record.

A global loader returns another tenant’s record

Why it happens
A request-dependent cache outlived its principal context.
How to fix it
Create scoped loaders and include needed identity dimensions.
Prevent it next time
Run sequential synthetic callers through isolation tests.

An auth-looking directive grants no protection

Why it happens
The schema declaration had no enforcing implementation.
How to fix it
Wire and test policy execution in the actual adapter.
Prevent it next time
Review directives as code contracts, not decorative labels.

Check your understanding

1. A root lookup is protected. Why test author.courses as well?

Show worked solution

The nested relationship may retrieve the same entity through a separate loader or service method. If that path omits visibility, clients can bypass the root restriction with a valid query. Exercise both paths using the same eligible and ineligible principal, and ensure list filtering and individual lookup preserve the documented information-disclosure policy.

2. What does the local predicate example fail to prove?

Show worked solution

It does not prove authentication, correct principal construction, secure transport, complete role policy or GraphQL middleware integration. It checks only three cases of a declared tenant/publication rule. That narrow evidence is still useful because it makes the policy decision reproducible, but combine it with adapter and operation tests before making a broader security claim.

Key takeaways

  • A graph policy must cover every route to an object.
  • Request context and cache lifetime are part of isolation.

Official references