Field-Level Permissions: Securing Sensitive Data in GraphQL
AI generated
{ }
type
GraphQL · Security · Authorization · Magento · Resolvers
Field-Level Permissions
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.

15 min read Resolver context · Null strategy · Auth exceptions · Magento GraphQL · Security · Headless Commerce

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.

11. FAQ: Field-Level Permissions in GraphQL

1Is query authentication enough on its own?
No. Ownership checks (does this resource belong to the current user?) must be implemented at the field level in the resolver.
2Return null or throw an exception?
Null for fields that are not accessible based on role. Exception for illegitimate requests (access to a resource belonging to someone else).
3How to prevent cross-customer data leaks?
Always apply the repository filter on customer_id = context.getUserId(). Never load all data and filter afterward in code.
4Are auth directives usable in Magento?
Not out of the box. Magento has no built-in directive middleware. A custom implementation is possible, but involved.
5Do admin fields need to be in the public schema?
Not necessary. Separate admin types or a separate endpoint can move admin fields out of the public schema.
6How to test auth scenarios systematically?
Explicit tests: customer A requests customer B's data, a guest requests protected fields, a customer requests admin fields. Each scenario needs a defined expected output.
7Should a security field be nullable or non-null?
Nullable. Non-null fields with an auth check can break the entire parent object resolution on failure. Nullable allows null as a correct response.
8Which Magento exception for auth errors?
GraphQlAuthorizationException, produces category: graphql-authorization in the errors array. Frontend and monitoring can classify it correctly.
9Does disabling introspection protect anything?
No. It only prevents systematic schema exploration. Auth gaps in resolvers are not fixed by this, only harder to find.
10Most important security practice rule?
The schema describes what is possible. The resolver decides on every request what gets returned to this caller. Security is resolver logic, not a schema feature.