GraphQL Authentication: Tokens, Sessions and Field-Level Auth
AI generated
{ }
type
GraphQL · Authentication · JWT · Field-Level Auth · Magento
GraphQL Authentication:
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.

15 min read JWT · Bearer · Context Object · Field Guard · Magento Auth GraphQL · Magento 2 · PHP

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.

11. FAQ: GraphQL Authentication, Tokens, Sessions and Field-Level Auth

1Can authentication be handled at the HTTP middleware level?
Partially. Token validation can happen in middleware, but the field-precise access decision still has to be made in the resolver.
2Difference between graphql-authorization and graphql-authentication?
authentication: no valid token present. authorization: token present, but the role or scope is not sufficient.
3How does token validation work in Magento GraphQL?
The Authorization header is checked on every request, the token is validated against the oauth_token table, and the customer ID is loaded into the GraphQlContext.
4How do you implement field-level auth in Magento?
Check the role inside the resolver, return null for fields the user is not authorized to see, or throw an exception. Magento has no built-in schema directives for this.
5Can a resolver return null instead of throwing an error?
Yes, for optional fields. What matters is whether the frontend needs to distinguish between missing data and missing authorization.
6Why does Magento return HTTP 200 on auth failures?
Standard GraphQL behavior: always HTTP 200, the error information lives in the errors array of the JSON response.
7How do you invalidate a Magento token immediately?
Via the revokeCustomerToken mutation. Opaque tokens can be invalidated instantly, JWTs cannot.
8What is a guest token in Magento GraphQL?
Not a real token, but a maskedCartId for anonymous carts. Guests are represented as USER_TYPE_GUEST in the context.
9Are schema directives better for auth?
More declarative in Apollo/Node.js, but framework dependent. Magento has no native support, so resolver guards remain the standard.
10How do I test auth logic in Magento resolvers?
Context as a mock with controlled getUserId()/getUserType() values in unit tests. A real token via generateCustomerToken in integration tests.