Tokens, Sessions and Field-Level Auth
Authentication in GraphQL is not a special case, it is part of every resolver that touches user-specific data. Anyone who fails to explicitly model token validation, session context and field-precise access control is building security gaps into the schema before the first query ever reaches production.
Table of Contents
- 1. Why authentication in GraphQL works differently than in REST
- 2. Token types: JWT, bearer and opaque session tokens compared
- 3. The context object: validate authentication once, use it everywhere
- 4. Resolver guards: securing access inside the resolver
- 5. Field-level auth: locking fields down by role and scope
- 6. Authentication in Magento GraphQL: token mutation and context
- 7. Error format for auth failures: setting extensions.category correctly
- 8. Auth approaches in direct comparison
- 9. Common authentication mistakes and how to spot them
- 10. Summary
- 11. FAQ
1. Why authentication in GraphQL works differently than in REST
In REST APIs, authentication is usually handled at the route level: a middleware layer checks the bearer token before the controller code runs. GraphQL typically exposes only a single endpoint, /graphql, that accepts every operation. That means the decision whether a request is authorized can no longer be made across the board at the HTTP level. It has to happen inside the resolver itself, or in a middleware layer that runs before the execution engine.
This shift has direct consequences for schema design and resolver architecture. Fields that only make sense for logged-in users must be checked explicitly in the resolver, because a missing guard leads to silent data exposure: GraphQL has no built-in auth mechanism at the field level. The result is that authentication in GraphQL is not an infrastructure decision, it is a design topic that has to be consciously addressed for every field and every resolver.
2. Token types: JWT, bearer and opaque session tokens compared
The three most common approaches to authentication in GraphQL APIs are JWT (JSON Web Tokens), opaque bearer tokens and server-side sessions. JWT tokens carry all relevant information (user ID, roles, expiry time) inside the token itself and do not need to be validated against a database. That makes them stateless and easy to scale, but it also demands careful handling of token rotation and invalidation, because an issued JWT stays valid until it expires.
Opaque bearer tokens are random strings that are mapped to a session on the server side. Every validation requires a database lookup or a cache lookup, but in exchange the token can be invalidated instantly. Magento uses this model: the generateCustomerToken mutation issues an opaque token that is sent along with the Authorization header on every subsequent request and checked against the database. Server-side sessions via cookie are less common in a GraphQL context, but they do show up in hybrid setups where a browser frontend and a GraphQL API share the same session.
# Token generation, Magento customer login mutation
mutation GenerateCustomerToken($email: String!, $password: String!) {
generateCustomerToken(email: $email, password: $password) {
token
}
}
# Using the token: Authorization header in all subsequent requests
# Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
# Revoke token on logout
mutation RevokeCustomerToken {
revokeCustomerToken {
result
}
}
3. The context object: validate authentication once, use it everywhere
The context object is the central pattern for authentication in GraphQL. It is populated once when the execution engine is set up, typically in a middleware layer that reads the Authorization header, validates the token and extracts the user information, and it is then available to every resolver as a third argument. This means the token is not parsed in every single resolver, only once per request.
In Magento, GraphQlContext implements this pattern: it exposes the getUserId() method, getUserType() and further contextual information. A resolver that returns user-specific data checks at the start whether $context->getUserId() returns a valid value, and throws a GraphQlAuthorizationException otherwise. This pattern is consistent, testable and avoids duplicate token validation in every single resolver method.
4. Resolver guards: securing access inside the resolver
A resolver guard is an explicit check at the start of every resolver method that determines whether the calling user is allowed to see that field. The guard uses the context object and throws a typed exception when authorization is missing. Magento provides dedicated exception classes for this: GraphQlAuthorizationException for missing authorization and GraphQlAuthenticationException for missing identity. The distinction matters, because it is reflected in the HTTP status code and in the extensions.category field of the error response.
A common anti-pattern is a missing guard on queries that look harmless from the outside. A product query that returns no user-dependent data does not need an auth guard. But as soon as a resolver returns customer data, price groups or personalized content, the guard has to be there, regardless of whether the frontend even shows the corresponding button. Security at the GraphQL level must never rely on the assumption that only authorized clients will send the query.
# Query that requires authentication, must be guarded in the resolver
query CustomerProfile {
customer {
firstname
lastname
email
addresses {
street
city
postcode
}
}
}
# Expected error response when the token is missing or invalid
# HTTP 200 (GraphQL always returns 200), error in the payload:
# {
# "errors": [{
# "message": "The current customer is not authorized.",
# "extensions": { "category": "graphql-authorization" }
# }],
# "data": { "customer": null }
# }
5. Field-level auth: locking fields down by role and scope
Field-level auth goes one step further than resolver guards: instead of only securing the resolver as a whole, individual fields within a type can be made accessible differently for different roles. A classic example is a User type where the email field is visible to regular users, but the internalScore field is only visible to administrators. Without explicit field-level auth, GraphQL returns every requested field that the resolver produces.
In practice, field-level auth is implemented in two ways: either sensitive fields are encapsulated in a separate type that only resolves for certain roles, or the resolver itself returns null for fields the user has no access to. The second approach is easier to implement but less explicit. A third option is schema directives such as @auth(requires: ADMIN), which run before the resolver call and automatically return null when authorization is missing.
6. Authentication in Magento GraphQL: token mutation and context
Magento implements a complete token-based auth system for GraphQL. Customers generate a token via generateCustomerToken, administrators via generateCustomerTokenAsAdmin. The token is embedded as a bearer header in all subsequent requests. Magento validates the token on every request, populates GraphQlContext and makes the user information available to every resolver.
An important aspect in the Magento context is the guest-versus-customer scenario: many Magento GraphQL endpoints accept both authenticated and unauthenticated requests, but return different data. The cart resolver, for example, uses the maskedCartId (guest) versus a customer-bound cart ID to determine whether a logged-in user or a guest is fetching their cart data. Resolvers must handle this distinction explicitly and must never assume that every caller is authenticated.
7. Error format for auth failures: setting extensions.category correctly
GraphQL errors from failed authentication should carry structured information in the extensions field of the error response. Magento relies on the category property inside the extensions object for this: graphql-authorization for missing authorization and graphql-authentication for missing identity. This categorization lets the frontend detect auth errors programmatically and react accordingly, for example by redirecting to login or by showing an error message.
A common mistake is returning auth problems as generic errors that carry no information about the nature of the problem. That makes error diagnosis harder and forces the frontend to rely on heuristics. The correct pattern is to throw typed exceptions that the framework converts into structured extensions data. At the same time, no internal stack traces or system information should ever end up in the error response, only the information the client actually needs to react sensibly.
# Structured auth error response, correct format with extensions.category
{
"errors": [
{
"message": "The current customer is not authorized to access this resource.",
"locations": [{ "line": 2, "column": 3 }],
"path": ["customer"],
"extensions": {
"category": "graphql-authorization"
}
}
],
"data": {
"customer": null
}
}
# Frontend can detect auth errors by checking extensions.category:
# if (error.extensions?.category === 'graphql-authorization') { redirectToLogin() }
8. Auth approaches in direct comparison
The choice of authentication approach has a direct impact on scalability, security and implementation effort. None of these approaches is universally superior, the right choice depends on the concrete use case.
| Approach | Advantage | Disadvantage | Typical use |
|---|---|---|---|
| JWT (stateless) | No DB lookup, scales horizontally | No instant revoke possible | Headless APIs, microservices |
| Opaque bearer | Instant revoke via DB | DB lookup on every request | Magento, classic shops |
| Session cookie | Browser-native, simple | Requires CSRF protection | Server-rendered plus GraphQL hybrid |
| Field directive | Declarative, easy to read | Framework dependency | Node.js, Apollo Server |
| Resolver guard | Explicit, framework independent | Repetitive, easy to forget | Magento, PHP APIs |
In the Magento context, the resolver guard combined with an opaque bearer token is the standard. Any deviation from that, for example a JWT-based auth system for an external frontend, requires the token validation layer inside GraphQlContext to be adapted accordingly. Anyone who validates JWTs externally and injects the result into the Magento context can reuse the existing guard pattern without changing the resolvers.
9. Common authentication mistakes and how to spot them
The most common mistake in GraphQL authentication is a missing guard on resolvers that are implicitly treated as non-public but were never explicitly secured. This mistake often shows up on newly added fields, because the developer forgets to replicate the guard from the parent resolver. One helpful convention: every resolver that touches customer data, price groups or personalized content must have a guard as its first statement, before any other logic runs.
A second widespread mistake is using the same token type for different user classes without a clear separation. If a guest token and a customer token are structurally identical, it is easy to end up with a resolver that accepts a guest token as if it were a customer token. The correct pattern is to check getUserType() explicitly and route different user classes into dedicated paths. Magento distinguishes here between UserContextInterface::USER_TYPE_CUSTOMER and USER_TYPE_GUEST.
# WRONG: no guard, exposes customer data to unauthenticated requests
# resolver returns customer data without checking context.getUserId()
# RIGHT: explicit guard as the first statement in the resolver
# PHP resolver pattern (Magento):
#
# public function resolve(Field $field, $context, ResolveInfo $info, ...): array
# {
# // Guard: check authentication before any business logic
# if (false === $context->getExtensionAttributes()->getIsCustomer()) {
# throw new GraphQlAuthorizationException(
# __('The current customer is not authorized.')
# );
# }
# // Business logic only reached if authenticated
# return $this->customerDataProvider->get($context->getUserId());
# }
# WRONG: same error response for a missing token and a wrong role
# { "errors": [{ "message": "Error" }] }
# RIGHT: differentiated error with extensions.category
# { "errors": [{ "message": "...", "extensions": { "category": "graphql-authorization" } }] }
10. Summary
Authentication in GraphQL requires a different mindset than in REST APIs: no route-level guard is enough, because all operations run through the same endpoint. The context object solves this problem by validating the token once per request and making the result available to every resolver. Resolver guards at the start of every relevant method ensure that user-specific data is never delivered without an explicit check. Field-level auth extends this pattern to fields within a type and enables role-based visibility without fragmenting the schema.
Magento ships a complete foundation with generateCustomerToken, GraphQlContext and the typed auth exceptions, and it should be used consistently. Common mistakes, such as missing guards on newly added fields, no distinction between guest and customer, or generic instead of categorized error responses, usually happen when auth is treated as an afterthought rather than as part of the resolver design from the start.
GraphQL Authentication, the essentials at a glance
Context object
Validate the token once, make the result available to every resolver through the context object. No token parsing in every single resolver.
Resolver guard
Explicit auth check as the first statement in every resolver that returns user-specific data. Never implicitly assume a caller is authorized.
Field-level auth
Encapsulate sensitive fields in separate types or return null when the role is not sufficient. Schema directives are more declarative but framework dependent.
Error format
extensions.category: graphql-authorization for a missing role, graphql-authentication for missing identity. No internal stack trace in the response.