Securing Sensitive Data in GraphQL
GraphQL authorization does not end at the query check. Sensitive fields such as email addresses, order histories, and admin-specific data must be secured at the field level, inside the resolver context, not just in the schema. This article shows how that works in practice.
Table of Contents
- 1. Why Query-Level Auth Is Not Enough
- 2. The Resolver Context as the Auth Carrier
- 3. Three Patterns for Field-Level Permissions
- 4. Null Strategy: When Null, When Exception?
- 5. Magento: Securing Customer, Order, and Admin Fields
- 6. Schema Design for Security-Sensitive Fields
- 7. Auth Middleware and Directives: Pros and Cons
- 8. Testing Auth Scenarios Systematically
- 9. Comparing Auth Approaches at a Glance
- 10. Summary
- 11. FAQ
1. Why Query-Level Auth Is Not Enough
A common assumption is that if a query is only accessible to logged-in users, all fields inside it are automatically protected. That holds true for simple cases, but not once a schema contains fields that should be populated differently depending on the user's role. A customer account type in Magento contains both publicly irrelevant fields like the first name and sensitive fields like the full address list, the email address, or the order history. Query-level auth alone does not prevent a logged-in customer from querying another customer's data.
There is a second scenario on top of that: admin fields. Many GraphQL schemas contain fields that technically exist but are only relevant to certain roles, internal scores, debug information, customer IDs for CRM systems. These fields show up in the schema and are theoretically queryable, even though a regular customer should not have access. The solution is not to hide them from the schema, but to check the resolver context at the field level.
2. The Resolver Context as the Auth Carrier
In Magento GraphQL, the resolver context is the central auth object. It is built on every request and contains the current customer's user ID, the user type (guest, customer, admin), and other context data. The context is accessible in the resolver through the second parameter and should be checked in every resolver that accesses personal data.
The pattern: the resolver first calls $context->getUserId(). If the result is null or zero, the caller is not authenticated. If the user ID is present, the resolver checks whether the requested data belongs to the logged-in user. Only if both checks pass are the sensitive fields populated. This logic must live explicitly in the resolver, the schema itself cannot enforce it, because GraphQL schemas do not perform runtime checks.
# Customer type with mixed sensitivity fields
type Customer {
id: ID!
firstname: String!
lastname: String!
email: String! # sensitive: own customer only
addresses: [CustomerAddress] # sensitive: own customer only
orders: [Order] # sensitive: own customer only
internalCrmId: String # admin-only: null for regular customers
fraudScore: Float # admin-only: null for regular customers
}
# Query entry point (authentication is checked at top-level resolver)
type Query {
customer: Customer # requires Bearer token
}
3. Three Patterns for Field-Level Permissions
In practice, three field-level auth patterns have become established, each suited to a different context. The first pattern is direct context checking in the resolver: the resolver compares the requested resource against the caller's user ID and throws a GraphQlAuthorizationException if the IDs do not match. This is the safest pattern because it makes no implicit assumptions.
The second pattern is returning null for unauthorized fields: instead of an exception, the resolver returns null if the caller is not authorized. This works well for optional admin fields where a regular customer simply sees no data, without receiving an error. The third pattern uses auth directives (@auth(requires: ADMIN)) at the schema level, interpreted through middleware. This is declarative and maintainable, but requires a middleware implementation and does not work out of the box in Magento.
4. Null Strategy: When Null, When Exception?
The decision between null and exception at the field level has a direct impact on frontend behavior. In standard GraphQL, an exception breaks the entire parent object resolution: if a non-null field throws an exception, the error propagates upward through the response tree and can set the entire object to null. That is often not desirable. The principle: sensitive fields subject to auth checks should be declared nullable (String, not String!).
An auth exception should only be thrown when the entire request is illegitimate, for example when a customer tries to query another customer's data through a top-level query argument. For fields that are simply not available for the current user role, null is the more correct response. The client then knows: the field exists but is not accessible to me, and can react accordingly without ending up in an error state.
5. Magento: Securing Customer, Order, and Admin Fields
In Magento GraphQL there are several critical places where field-level auth is essential. The most prominent example is the customer query: it returns the logged-in customer's data based on the Bearer token in the Authorization header. The top-level resolver checks the context and only loads the customer whose ID matches the token. Even so, fields like email and addresses are only fully secure after a second check inside the field resolver.
Securing order data is even more critical. The customerOrders query must check not only whether the caller is logged in, but whether every single returned order actually belongs to that customer. A bug in this area can result in another customer's order details being returned, a serious privacy issue. The pattern: always apply an explicit filter condition on customer_id = context.getUserId() in the repository query, never rely solely on the schema level or the query entry point.
# Secure resolver pattern for Magento field-level authorization
# File: Model/Resolver/CustomerEmail.php
# The resolver checks both authentication and ownership:
# 1. Is the user authenticated? context.getUserId() > 0
# 2. Does the requested customer belong to this user?
# 3. Only then return the sensitive field
# Example authorization flow in pseudo-GraphQL terms:
query SecureCustomerData {
customer {
id # safe: only own customer loaded at top-level resolver
firstname # safe: no extra auth needed
email # sensitive: resolver re-checks context.getUserId()
orders { # sensitive: repository filters by customer_id
number
grand_total
status
}
internalCrmId # admin-only: resolver returns null unless isAdmin()
}
}
# Response when accessing internalCrmId as regular customer:
# { "data": { "customer": { "internalCrmId": null } } }
# No error, just null, frontend handles gracefully
6. Schema Design for Security-Sensitive Fields
The schema implicitly communicates expectations to the client. A field declared as String! promises: this value is always present. For security-sensitive fields, the rule is: if the value can be missing depending on the auth context, the field must be nullable. That is not a weakness of the schema, it is correct modeling of reality. A field like internalScore: Float signals to the client: this field may be present, but does not have to be.
It is also worth applying the type separation pattern: move admin-specific fields into a separate type such as AdminCustomerData that is only reachable through an admin-authenticated query. The schema itself can be structured so that admin fields never appear in the public schema for regular clients at all, through a separate endpoint or a schema stitching strategy. That is more maintainable than dozens of individual null checks at the field level.
7. Auth Middleware and Directives: Pros and Cons
Auth directives like @auth(requires: ADMIN) are an elegant way to express permissions declaratively in the schema. Instead of building explicit checks into every resolver, a middleware processes all fields with an @auth directive before the actual resolver execution. This centralizes auth logic and makes security requirements readable directly in the schema.
The downside: Magento has no built-in directive middleware for auth. A custom implementation is possible but involved. On top of that, directive-based auth does not know the context state at compile time, dynamic checks like "does this order belong to the current customer" cannot be expressed in a static directive. The takeaway: directives are well suited for role-based checks (admin vs. customer), not for resource-based ownership checks. For the latter, explicit context checking in the resolver remains the safer and more flexible choice.
8. Testing Auth Scenarios Systematically
Auth bugs in GraphQL are often hard to find because the API works correctly in happy-path tests but leaks data in cross-customer scenarios. Systematically testing field-level permissions requires explicit test scenarios for every critical auth path. That means: test as logged-in customer A trying to retrieve customer B's data. Test as a guest requesting a protected field. Test as a customer requesting admin fields. Each of these scenarios should have a defined expected output, either null or an auth exception.
In Magento, integration tests using the GraphQl test client from the test framework work well for this. For each scenario a dedicated customer is created, a token is generated, and a query is fired. The response is validated against the expected structure. Especially important: the auth tests must run after every schema change and resolver refactor. GraphQL Inspector can additionally check whether newly added fields are modeled as nullable in the schema when they are subject to auth checks.
# Test scenarios for field-level authorization
# Scenario 1: Authenticated customer requests own data (PASS)
# Authorization: Bearer <customer-a-token>
query OwnData {
customer {
email # returns: "customer-a@example.com"
orders { number } # returns: customer A's orders only
}
}
# Scenario 2: Guest requests protected field (FAIL, exception)
# No Authorization header
query GuestAccess {
customer {
email # throws GraphQlAuthorizationException
}
}
# Scenario 3: Customer requests admin-only field (null, no exception)
# Authorization: Bearer <customer-a-token>
query AdminFieldAsCustomer {
customer {
fraudScore # returns: null (customer cannot see this)
}
}
9. Comparing Auth Approaches at a Glance
Real-world projects often combine several auth patterns at once. The right choice depends on the type of check: role-based or resource-based, declarative or imperative. The following table summarizes the most important approaches.
| Auth Approach | Check Level | Suited For | Limitation |
|---|---|---|---|
| Top-level resolver check | Query entry point | Basic auth: logged in vs. guest | Does not protect individual fields |
| Field resolver context check | Individual field | Ownership checks, sensitive fields | Repeated across many resolvers |
| Null return | Field value | Optional fields for specific roles | Requires nullable declaration |
| Auth directives | Schema declaration | Role-based fields | No Magento builtin, no ownership logic |
| Separate admin schema types | Schema structure | Clear separation of admin vs. customer | Higher schema design effort |
In Magento projects, combining a top-level resolver check for basic auth with explicit field resolver checks for ownership and admin fields is the most proven strategy. Auth directives can be used as a complement for simple role checks when the infrastructure for it exists. Separate admin types are especially useful when the schema is publicly documented and admin fields should not be visible in the public schema.
Mironsoft
GraphQL security, auth architecture, and Magento resolvers
Need sensitive data in GraphQL reliably secured?
We analyze existing GraphQL resolvers for auth gaps, implement field-level permissions, and systematically test all critical auth scenarios, for Magento and headless frontends.
Auth audit
Analyze resolvers for missing context checks and ownership gaps
Field-level auth
Implement context checks, null strategy, and admin types for sensitive fields
Auth testing
Systematically test cross-customer scenarios, guest access, and admin fields
10. Summary
Field-level permissions in GraphQL are an essential part of a secure API architecture, and one that gets neglected in many projects. The core insight: authentication at the query level does not prevent a logged-in user from seeing data that does not belong to them. Ownership checks, admin field checks, and the right combination of exception and null return must be implemented explicitly at the field level.
The most important principle: security is not a schema feature, it is resolver logic. The schema describes what is theoretically present. The resolver decides on every request what is actually returned to this specific caller. Anyone who understands and consistently applies this separation builds GraphQL APIs that hold up under GDPR requirements and penetration test scenarios.
Field-Level Permissions in GraphQL, the Key Points at a Glance
Context check
Check context.getUserId() in every field resolver that accesses personal data. Never rely on the top-level check alone.
Null vs. exception
Exception for illegitimate requests. Null for fields not accessible to the current role. Fields must be declared nullable.
Ownership check
Always apply the repository filter on customer_id = context.getUserId(). Never check only the query entry point.
Testing
Set up cross-customer tests, guest access tests, and admin field tests as their own test classes and run them after every refactor.