Authentication & Authorization in GraphQL
🔐 Authentication & Authorization in GraphQL
UNAUTHENTICATED error. This chapter is about implementing auth properly — authentication (who's asking) and authorization (what they're allowed to see) in an API with exactly one endpoint, building on the session/JWT material from the site's Authentication & Session Security course.
Authentication Happens Once, in Context
Unlike REST, where each route can independently apply its own auth middleware, GraphQL has one endpoint — so authentication happens exactly once, inside the context function from Chapter 6:
Every resolver in the request now sees the same context.currentUser — either a real user or null — without needing its own separate auth check to establish who's asking.
🎯 Authorization Happens Per Field
A single query can mix public and protected fields, so authorization can't live in one central gate the way REST middleware does — it belongs inside whichever specific resolvers actually need it:
A Field Only Visible to the Owner or an Admin
Return null vs Throw
Returning null for a nullable field lets the rest of the query still succeed — good for "you're just not allowed to see this one thing." Throwing (Chapter 7) is clearer but forces the client to specifically handle the error.
Reusable Authorization Helpers
Rather than repeating checks in every resolver, wrap them: requireAuth(context), requireOwnerOrAdmin(context, ownerId) — called at the top of any resolver that needs them.
One Gate vs Many Small Checks
REST
Auth middleware runs once, before a route handler — the whole endpoint is protected or it isn't.
GraphQL
Checks are scattered across whichever resolvers need them, since one query commonly mixes public and protected fields in a single request.
Some libraries offer schema-level @auth-style custom directives as an alternative to hand-written per-resolver checks — worth knowing exists, though this course sticks to explicit resolver-level checks for clarity.
💻 Coding Challenges
Challenge 1: Write requireOwnerOrAdmin
Write a reusable requireOwnerOrAdmin(context, resourceOwnerId) function that throws a FORBIDDEN-coded GraphQLError unless context.currentUser is either the resource's owner or has an ADMIN role.
Goal: Practice generalizing the chapter's inline ownership check into a reusable helper.
Challenge 2: Choose Null vs Throw
For each of (a) a User.email field hidden from non-owners, and (b) an updatePost mutation called by someone who doesn't own the post, recommend returning null or throwing, and justify each choice.
Goal: Practice applying the null-vs-throw tradeoff to two different kinds of operations.
Challenge 3: Find the Unprotected Path
A schema checks authorization only inside Query.user's resolver, assuming this protects every User field from unauthorized access. Explain a different query path that reaches the same User data without ever running that check.
Goal: Practice recognizing that the same type can be reached through multiple different resolver paths.
It's tempting to add an authorization check to Query.user's resolver and consider User data protected — but that's only one path to reach a User object. Query.posts → Post.author returns full User objects too, through a completely different resolver that never runs Query.user's check at all. In GraphQL, the same type is frequently reachable through many different nested paths, and a check placed on one entry point does nothing to protect any of the others. Authorization needs to live on the field actually returning sensitive data (like User.email in this chapter's own example) — not just on whichever root query happens to be the "obvious" way in.
🎯 What's Next
Every piece is now in place — the final chapter is a capstone: Building a Small GraphQL API, combining the schema, resolvers, mutations, DataLoader, and auth from every chapter into one working service.