Skip to content

Authentication & Context

The first of our four questions is who is asking? Answering it is authentication: turning an opaque credential — usually a token in an HTTP header — into a known identity. The key GraphQL idea is where that work happens. You do it once per request, before any resolver runs, and you hand the result to every resolver through a shared object called the context.

When a GraphQL server executes a request, it threads a single context value through every resolver. You have already seen the signature: a resolver receives (parent, args, context, info). The context is yours to fill. It is the natural home for anything that is true for the whole request but not part of the schema: the current user, a database connection, a request-scoped cache, a logger.

Authentication populates the context. A small function — often called a context factory — runs once when the request arrives. It reads the Authorization header, verifies the token, looks up the user, and returns a context object. From then on, every resolver simply reads context.user.

sequenceDiagram
  participant C as Client
  participant F as Context factory
  participant V as Token verifier
  participant R as Resolvers
  C->>F: Request + Authorization header
  F->>V: Verify token (once)
  V-->>F: user (or null if anonymous)
  F-->>R: context = { user }
  R->>R: read context.user
  R-->>C: Response
The token is verified once at the edge; resolvers only ever read the resulting identity.

Authenticate at the edge, not in the schema

Section titled “Authenticate at the edge, not in the schema”

A common beginner mistake is to put authentication inside a field — adding a login(token: String) field, or having each resolver re-parse the Authorization header. Resist it. The schema describes your data; it should not describe how credentials are decoded. Decode the token once, at the edge, and let the schema stay clean:

// Runs once per request, before any resolver. With GraphQL Yoga or Apollo
// Server you supply this as the `context` factory.
async function createContext({ request }: { request: Request }) {
const header = request.headers.get('authorization') ?? '';
const token = header.replace(/^Bearer\s+/i, '');
// verifyToken throws or returns null for bad/expired tokens.
const user = token ? await verifyToken(token) : null;
// Note: we do NOT throw here for anonymous requests. Some fields are
// public. Authentication establishes identity; it does not grant access.
return { user };
}

Two design points are worth underlining. First, the factory returns a user of null for anonymous requests rather than rejecting them — plenty of fields (a public product catalogue, a marketing page query) are meant to be reachable without logging in. Authentication answers “who,” not “may they”; the “may they” decision is authorization, the subject of the next lesson. Second, the token is verified exactly once, so an expensive operation (signature verification, a user lookup) does not repeat for every field in a large query.

The example below builds a tiny schema with two fields. me returns the name of the authenticated user, or null when nobody is logged in. serverTime is public and ignores identity entirely. Crucially, the resolvers never touch a token — they only read context.user, which a context factory resolved up front. Press Run to execute it with the real GraphQL engine; the context is passed in as contextValue.

JavaScript

Look at the two outputs. With a verified user on the context, me returns "Ada Lovelace". With user: null, me comes back as null while serverTime still works — the public field does not care who is asking. The resolvers stayed identical between the two runs; only the context changed. That is the whole point: identity is established once, outside the resolvers, and the schema reads it like any other request-scoped fact.

Where should a GraphQL server verify the incoming token and resolve the current user?
What should the context factory return for an anonymous request that carries no valid token?
In the runnable example, why did the me resolver not need to read the Authorization header?