Magento 2 GraphQL Customer Login, Tokens and Security
A headless storefront or mobile app on Magento needs customer accounts: registration, login, logout, password reset, and authenticated requests for the cart, orders and addresses. Magento GraphQL handles all of it with bearer tokens. The mutations are simple. The decisions around them, such as where to store the token, how long it lives, and what to do when it expires, are where projects go wrong. This article covers both for Magento 2.4.8.
How customer tokens work
A customer logs in by calling generateCustomerToken with email and password. Magento returns a token. Every later request that needs the customer context sends it in the Authorization header as a bearer token. There is no session cookie involved in this flow.
mutation Login($email: String!, $password: String!) {
generateCustomerToken(email: $email, password: $password) {
token
}
}
curl 'https://shop.example.com/graphql' \
-H 'Content-Type: application/json' \
-H 'Store: default' \
-H 'Authorization: Bearer <customer token>' \
-d '{"query":"{ customer { firstname lastname email } }"}'
Since Magento 2.4.4 these tokens are JSON Web Tokens signed by Magento. The client should treat the token as opaque. Do not decode it in the frontend to make decisions, and do not rely on its claims staying the same across versions. The same token type is used for the REST API, which is covered in more depth in Web API authentication and tokens.
Token lifetime
Customer token lifetime is set in the admin under Stores, Configuration, Services, OAuth, Access Token Expiration, in the Customer Token Lifetime field (in hours). The same value can be set from the CLI.
bin/magento config:set oauth/access_token_lifetime/customer 4
Choose this with the storefront in mind. A short lifetime is safer but logs customers out in the middle of a session unless the frontend handles it well. Magento does not issue refresh tokens for customers. When the token expires the customer has to log in again, or your backend for frontend has to manage a longer session of its own and ask the customer to log in when Magento rejects the token.
Logout
mutation {
revokeCustomerToken { result }
}
Call it with the token in the header. Then delete the token on the client. Revoking matters because a token copied from a shared device would otherwise keep working until it expires.
Registration
New accounts are created with createCustomerV2. The input accepts the standard customer attributes and newsletter subscription. Password rules (length, character classes) come from the store configuration, so the frontend should show Magento's error messages rather than duplicate the rules.
mutation Register($input: CustomerCreateInput!) {
createCustomerV2(input: $input) {
customer { firstname lastname email }
}
}
{
"input": {
"firstname": "Ana",
"lastname": "Rossi",
"email": "[email protected]",
"password": "a long and unique passphrase",
"is_subscribed": false
}
}
Registration does not log the customer in. Call generateCustomerToken afterwards. If the store requires email confirmation for new accounts, login will fail until the customer confirms, and the frontend needs a page for that state.
Password reset and change
Reset is a two step flow. First request the email.
mutation Forgot($email: String!) {
requestPasswordResetEmail(email: $email)
}
Magento sends its standard reset email with a link that points to the Magento storefront URL by default. In headless setups that link must point to the frontend. Edit the email template or the URL it uses so customers land on a page in your app that reads the token from the URL and calls resetPassword.
mutation Reset($email: String!, $token: String!, $password: String!) {
resetPassword(email: $email, resetPasswordToken: $token, newPassword: $password)
}
Logged in customers change their password with changeCustomerPassword, which requires the current password. The email is changed with updateCustomerEmail, which also requires the password. Other profile fields go through updateCustomerV2.
Always show the same message for the reset request whether or not the email exists. Otherwise the form becomes a way to find out which emails have accounts.
Where to store the token
This is the decision with the biggest security impact.
- localStorage or sessionStorage is the easiest option and the most common in tutorials. Any script that runs on the page can read it, so a single XSS issue or compromised third party script exposes customer tokens.
- An HTTP only cookie set by your own server is safer. The browser sends it to your frontend server, which adds the bearer header when it calls Magento. JavaScript in the page never sees the token. This is the pattern I recommend for any server rendered headless storefront, such as the one in headless Magento with Next.js.
- Mobile apps should use the platform's secure storage (Keychain on iOS, Keystore backed storage on Android).
With HTTP only cookies, set Secure and an appropriate SameSite value, and protect your own mutation endpoints against CSRF.
Handling expired tokens
When a token is invalid or expired, Magento returns a GraphQL error whose extensions.category is graphql-authorization. Your client should catch that category in one place and:
- Clear the stored token.
- Keep the guest state usable, including a new guest cart if needed.
- Send the customer to login with a return URL, but only when the action needs an account.
A frontend that checks this per component will end up with pages that silently show empty data. Handle it in the GraphQL client wrapper.
Brute force protection
Login, registration and password reset are attractive targets for credential stuffing. Several layers apply:
- Account lockout. The settings under Stores, Configuration, Customers, Customer Configuration, Password Options (maximum login failures and lockout time) apply to
generateCustomerTokentoo. - reCAPTCHA. Magento's reCAPTCHA modules can protect GraphQL operations such as customer login, account creation and password reset. The frontend sends the reCAPTCHA response in an
X-ReCaptchaheader. GraphQL support is limited to specific reCAPTCHA types, so check the documentation for your version when you configure it under Stores, Configuration, Security, Google reCAPTCHA Storefront. - Rate limiting at the web server or CDN for the
/graphqlendpoint, keyed on the operation if your edge can inspect it, or at least on IP.
Customer data queries
With a token, the customer query returns the profile, addresses, orders and more. Request only what each page needs. The order history query in particular can be heavy if you request every item on every order.
query Orders($page: Int!) {
customer {
orders(pageSize: 10, currentPage: $page) {
items {
number
order_date
status
total { grand_total { value currency } }
}
page_info { current_page total_pages }
}
}
}
Customer queries must never be cached by a shared cache. Send them as POST or with no store options so neither Varnish nor your frontend caches them. If you want caching behavior for catalog queries made by logged in customers, see the X-Magento-Cache-Id handling in GraphQL caching and performance.
Logging in as a customer from the admin
Support teams sometimes need to see what a customer sees. In Adobe Commerce, the remote shopping assistance feature provides generateCustomerTokenAsAdmin, which creates a customer token from an admin integration when the customer has allowed assistance. This mutation is Adobe Commerce only. Magento Open Source has the Login as Customer feature for the regular storefront, but a headless frontend needs its own handling for it.
After login: the cart
Right after a successful login, read customerCart and merge any guest cart into it with mergeCarts. Skipping this is the most common reason customers say items disappeared from their cart when they logged in. The full cart flow is in GraphQL cart and checkout mutations.
FAQ
How long does a Magento customer token last?
It depends on the Customer Token Lifetime setting under Stores, Configuration, Services, OAuth, Access Token Expiration. Magento does not issue refresh tokens, so the customer logs in again after expiry.
Where should a headless storefront keep the customer token?
In an HTTP only, Secure cookie set by your own server, which adds the bearer header when calling Magento. Avoid localStorage, which any script on the page can read.
How do I detect an expired token in GraphQL?
Magento returns an error with extensions.category set to graphql-authorization. Catch it centrally in your GraphQL client, clear the token and fall back to guest state.
Is generateCustomerTokenAsAdmin available in Open Source?
No. It belongs to the remote shopping assistance feature in Adobe Commerce.
I am available for freelance Magento and Adobe Commerce work. If you need help with headless authentication, get in touch.
Related articles
Magento 2 GraphQL Caching and Performance: Practical Guide
Make Magento 2 GraphQL fast: GET queries, Varnish caching, X-Magento-Cache-Id, @cache identities for custom resolvers, N+1 batching and query complexity limits.
Magento 2 GraphQL Checkout: Cart to Order Mutations
Full Magento 2 GraphQL checkout flow: guest and customer carts, addProductsToCart, addresses, shipping, payment, placeOrder, merging carts and common errors.
Headless Magento 2 with Next.js: Storefront Architecture
Build a headless Magento 2 storefront with Next.js: GraphQL client, route query for URLs, product pages, cart server actions, tag revalidation and SEO basics.
Magento 2 GraphQL: Add a Custom Query and Resolver
How to add a custom GraphQL query in Magento 2.4: schema.graphqls, a resolver with ResolverInterface, errors, auth checks and cache identity for Varnish.