Customer Data Securely
Integrating custom fields into the customer types, securing resolvers with token validation, exposing extension attributes correctly, and protecting sensitive data at the field level, all in one hands-on practical article.
Table of Contents
- 1. Why customer extensions in GraphQL are delicate
- 2. Understanding the customer schema in Magento
- 3. Exposing extension attributes cleanly in the schema
- 4. Building the resolver: delegation instead of logic
- 5. Token validation and context checking in the resolver
- 6. Field-level protection: what should never appear in the schema
- 7. Wrong versus right: typical security gaps
- 8. Integration tests for secured customer queries
- 9. Summary
- 10. Comparison: hardening strategies at a glance
- 11. FAQ
1. Why customer extensions in GraphQL are delicate
Customer data is among the most sensitive information an e-commerce system holds. Anyone extending the Magento customer schema with custom fields usually does so for a good reason: bonus points, loyalty tiers, preferences, or internal segmentation attributes need to be available in the headless frontend. The technical path there is manageable, but the security dimension is often underestimated. A resolver that performs no token check instantly turns a harmless field into a data leak vector.
In Magento, GraphQL security hinges on one central mechanism: the customer context, which is populated via the bearer token in the Authorization header. Every resolver that returns customer-related data must check this context, otherwise unauthenticated requests can retrieve other customers' data if the resolver does not implement explicit access control. This article shows how to extend the schema cleanly, build the resolver correctly, and avoid opening security holes along the way.
2. Understanding the customer schema in Magento
Magento defines the Customer type in Magento_CustomerGraphQl/etc/schema.graphqls. The schema already contains standard fields like firstname, lastname, email, and addresses. The customer query carries an authorization attribute: Magento checks internally whether a valid token is present before the resolver is invoked. Custom extensions must build on this type, via extend type Customer in a separate schema.graphqls file inside the module directory.
It's important to understand that merely adding a field to Customer does not by itself guarantee security. The actual protection happens in the resolver. Magento does supply the context, but whether the resolver actually evaluates that context is entirely up to the developer. A field that internally points to an EAV table combined with a resolver that performs no context check is a combination that causes problems in production.
# app/code/Vendor/CustomerExtension/etc/schema.graphqls
# Extend the native Customer type with a loyalty field
extend type Customer {
loyalty_level: String @resolver(class: "Vendor\\CustomerExtension\\Model\\Resolver\\LoyaltyLevel")
bonus_points: Int @resolver(class: "Vendor\\CustomerExtension\\Model\\Resolver\\BonusPoints")
internal_segment: String @resolver(class: "Vendor\\CustomerExtension\\Model\\Resolver\\InternalSegment")
}
3. Exposing extension attributes cleanly in the schema
Extension attributes are Magento's own mechanism for extending data models without core changes. In the GraphQL context that means: the EAV attribute or custom attribute lives on the CustomerInterface, and the resolver must read it from the correct source. The common mistake is accessing the $value array directly in the resolver and hoping the desired attribute is already present there. In practice, the $value array in Magento GraphQL is the resolved parent value; for customer fields it contains the customer object's data, but not automatically all extension attributes.
The clean solution is to access the CustomerInterface object in the resolver by extracting the entity_id from the $value array and using it to load the full customer model via a repository. That guarantees extension attributes are loaded correctly, including any plugin magic that kicks in during loading. Anyone who wants to avoid repeated loading for performance reasons can implement a request-scoped cache layer with its own identity map.
4. Building the resolver: delegation instead of logic
A good GraphQL resolver in Magento is thin. It accepts arguments, checks the context, delegates the actual work to a service or repository, and returns the result. Business logic does not belong in the resolver, it belongs in a service that can be tested independently of GraphQL. That holds for customer resolvers just as much as for product resolvers: the resolver is the translator between the GraphQL layer and the domain logic, not a container for both.
# Query to fetch loyalty data for the authenticated customer
query GetCustomerLoyalty {
customer {
firstname
email
loyalty_level
bonus_points
}
}
# Mutation to redeem bonus points
mutation RedeemPoints($points: Int!) {
redeemBonusPoints(points: $points) {
success
remaining_points
message
}
}
5. Token validation and context checking in the resolver
Magento provides a UserContextInterface in the resolver context. Via $context->getUserContext()->getUserType() the resolver can check whether the requesting user is an authenticated customer. The user type UserContextInterface::USER_TYPE_CUSTOMER signals that a valid token is present and belongs to a real customer. If the user type is a guest or an admin, the resolver should respond with a GraphQlAuthorizationException, not with an empty result. Empty results hide errors; exceptions make security problems visible.
In addition to the user-type check, it's important to take the customer ID from the context and make sure the resolver only returns the requesting customer's own data, never someone else's. For customer fields on the Customer type this happens implicitly in Magento, because the customer query always returns the logged-in customer. For custom queries that accept a customer ID as an argument, the explicit check is mandatory.
# Correct: customer query always returns the authenticated customer's data
query MyAccount {
customer {
id
email
loyalty_level
bonus_points
}
}
# Wrong: exposing a query that accepts arbitrary customer IDs without auth check
# query GetAnyCustomer($id: Int!) {
# customerById(id: $id) { email bonus_points }
# }
# This would allow any caller to read any customer's data
6. Field-level protection: what should never appear in the schema
Not everything that is technically available as an extension attribute belongs in the GraphQL schema. Internal scoring values, fraud detection features, raw password hashes, or administrative flags are examples of fields that have no place in the headless frontend. GraphQL introspection lets any client query the full schema; whatever is in the schema is visible to anyone who can enable introspection. Fields you don't want to expose simply must not appear in the schema.
A second category covers fields that may live in the schema but should only return data under certain conditions. Example: a loyalty_level should only be visible to customers who participate in the bonus program. The resolver checks that condition and returns null if it isn't met. That's cleaner than removing the field from the schema entirely, because frontend developers still know the field exists but is context-dependent.
7. Wrong versus right: typical security gaps
The most common mistake in customer resolver extensions is omitting the context check. A resolver that only evaluates the $value array and performs no auth check whatsoever returns data as soon as the type is resolved, regardless of whether a valid token is present. In Magento, the customer query itself is protected by an authorization attribute, but custom queries that expose customer data through other entry points can bypass that protection.
| Scenario | Wrong | Right | Risk |
|---|---|---|---|
| Auth check | No context check in the resolver | Check getUserType(), throw an exception | Data leak for unauthenticated calls |
| Customer ID | Using the ID from the argument unchecked | Take the ID from the context, ignore the argument | IDOR, access to other customers' data |
| Internal fields | Including the fraud score in the schema | Not exposing internal fields in the schema | Introspection reveals internal logic |
| Error handling | Returning null on an auth failure | Throwing GraphQlAuthorizationException | The error stays invisible, the client notices nothing |
| Resolver logic | Business logic directly in the resolver | Service delegation, the resolver only delegates | Hard to test, uncontrollable dependencies |
8. Integration tests for secured customer queries
Customer resolver tests in Magento are integration tests that issue a real HTTP request against the GraphQL endpoint. Magento provides the GraphQlQueryTest base class for this purpose. A solid test setup for secured customer fields covers at least three scenarios: a successful fetch with a valid token, a rejected fetch without a token that expects the correct error code, and an attempt to access another customer's data using a foreign token. The last test is the most important one, and also the one most often left out.
For quick manual verification during day-to-day development, Altair or GraphiQL with a saved token in the Authorization header is the most efficient approach. The token can be generated via the generateCustomerToken mutation in the same GraphiQL session and entered directly into the header section. This setup lets you test new fields immediately after a schema deploy, without having to write a separate test script.
# Step 1: Obtain a customer token
mutation Login {
generateCustomerToken(email: "test@example.com", password: "Password1!") {
token
}
}
# Step 2: Use the token in Authorization header: Bearer <token>
# Then query the extended customer fields
query CustomerLoyaltyTest {
customer {
email
loyalty_level
bonus_points
}
}
# Step 3: Test without token, expect GraphQlAuthorizationException
# Expected response: { "errors": [{ "message": "The current customer isn't authorized." }] }
9. Summary
Extending customer data via Magento GraphQL is not technically a heavy undertaking; the combination of extend type Customer, a custom resolver, and DI configuration takes only a handful of files. The real effort sits in the security layer: token validation, context checking, deciding which fields even belong in the schema, and making sure resolvers never return another customer's data. Anyone who thinks these aspects through deliberately builds an extension that also works reliably in production with real customer data.
The most important guiding question when designing any customer GraphQL extension is: what happens if an attacker calls this query without a token, or with someone else's token? If the answer to that is unsatisfying, either the resolver is missing the auth check or the field simply doesn't belong in the schema at all. Security in GraphQL is not an afterthought, it's a design property of the resolver.
Extending Magento GraphQL Customer Data Securely: The Essentials at a Glance
Schema Extension
extend type Customer in its own schema.graphqls, only include fields the frontend actually needs. Internal attributes do not belong in the schema.
Resolver Security
Always check getUserType(), throw GraphQlAuthorizationException on a missing token. Always take the customer ID from the context, never from query arguments.
Delegation
Resolvers delegate to services and repositories, no business logic in the resolver. Keep testability independent of the GraphQL layer.
Testing
Three scenarios: valid token, no token (expects an exception), foreign token (expects no foreign data). The third test is the one most often forgotten.