Login de cliente, tokens e segurança no GraphQL do Magento 2
Uma loja headless ou um app mobile no Magento precisa de contas de cliente: cadastro, login, logout, reset de senha e requisições autenticadas para carrinho, pedidos e endereços. O GraphQL do Magento cuida de tudo isso com bearer tokens. As mutations são simples. As decisões em torno delas, como onde guardar o token, quanto tempo ele vive e o que fazer quando ele expira, é onde os projetos erram. Este artigo cobre as duas coisas para o Magento 2.4.8.
Como os tokens de cliente funcionam
Um cliente faz login chamando o generateCustomerToken com e-mail e senha. O Magento devolve um token. Toda requisição seguinte que precisa do contexto do cliente o envia no header Authorization como bearer token. Não há cookie de sessão envolvido nesse fluxo.
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 } }"}'
Desde o Magento 2.4.4 esses tokens são JSON Web Tokens assinados pelo Magento. O cliente deve tratar o token como opaco. Não o decodifique no frontend para tomar decisões e não confie em que as claims dele continuem iguais entre versões. O mesmo tipo de token é usado na REST API, que é coberta com mais profundidade em autenticação e tokens da Web API.
Lifetime do token
O lifetime do token de cliente é definido no admin em Stores, Configuration, Services, OAuth, Access Token Expiration, no campo Customer Token Lifetime (em horas). O mesmo valor pode ser definido pelo CLI.
bin/magento config:set oauth/access_token_lifetime/customer 4
Escolha pensando na loja. Um lifetime curto é mais seguro, mas desloga clientes no meio de uma sessão se o frontend não tratar bem. O Magento não emite refresh tokens para clientes. Quando o token expira, o cliente precisa fazer login de novo, ou o seu backend for frontend precisa gerenciar uma sessão mais longa própria e pedir login ao cliente quando o Magento rejeitar o token.
Logout
mutation {
revokeCustomerToken { result }
}
Chame com o token no header. Depois apague o token no cliente. Revogar importa porque um token copiado de um dispositivo compartilhado continuaria funcionando até expirar.
Cadastro
Contas novas são criadas com o createCustomerV2. O input aceita os atributos padrão do cliente e a assinatura de newsletter. As regras de senha (tamanho, classes de caracteres) vêm da configuração da loja, então o frontend deve mostrar as mensagens de erro do Magento em vez de duplicar as regras.
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
}
}
O cadastro não faz o login do cliente. Chame o generateCustomerToken em seguida. Se a loja exige confirmação por e-mail para contas novas, o login vai falhar até o cliente confirmar, e o frontend precisa de uma página para esse estado.
Reset e troca de senha
O reset é um fluxo em duas etapas. Primeiro peça o e-mail.
mutation Forgot($email: String!) {
requestPasswordResetEmail(email: $email)
}
O Magento envia o e-mail padrão de reset com um link que, por padrão, aponta para a URL da loja Magento. Em configurações headless esse link precisa apontar para o frontend. Edite o template de e-mail ou a URL que ele usa para que os clientes cheguem a uma página do seu app que lê o token da URL e chama o resetPassword.
mutation Reset($email: String!, $token: String!, $password: String!) {
resetPassword(email: $email, resetPasswordToken: $token, newPassword: $password)
}
Clientes logados trocam a senha com o changeCustomerPassword, que exige a senha atual. O e-mail é alterado com o updateCustomerEmail, que também exige a senha. Os outros campos do perfil passam pelo updateCustomerV2.
Mostre sempre a mesma mensagem na solicitação de reset, o e-mail existindo ou não. Do contrário o formulário vira um jeito de descobrir quais e-mails têm conta.
Onde guardar o token
Essa é a decisão com maior impacto em segurança.
- localStorage ou sessionStorage é a opção mais fácil e a mais comum nos tutoriais. Qualquer script que rode na página consegue lê-lo, então uma única falha de XSS ou um script de terceiros comprometido expõe os tokens dos clientes.
- Um cookie HTTP only definido pelo seu próprio servidor é mais seguro. O navegador o envia ao seu servidor de frontend, que adiciona o header bearer quando chama o Magento. O JavaScript da página nunca vê o token. É o padrão que recomendo para qualquer loja headless renderizada no servidor, como a de Magento headless com Next.js.
- Apps mobile devem usar o armazenamento seguro da plataforma (Keychain no iOS, armazenamento com Keystore no Android).
Com cookies HTTP only, defina Secure e um valor adequado de SameSite, e proteja seus próprios endpoints de mutation contra CSRF.
Tratando tokens expirados
Quando um token é inválido ou expirou, o Magento devolve um erro GraphQL cujo extensions.category é graphql-authorization. Seu cliente deve capturar essa categoria em um único lugar e:
- Limpar o token guardado.
- Manter o estado de visitante utilizável, incluindo um carrinho de visitante novo se preciso.
- Enviar o cliente ao login com uma URL de retorno, mas só quando a ação exige uma conta.
Um frontend que confere isso por componente acaba com páginas que mostram dados vazios em silêncio. Trate no wrapper do cliente GraphQL.
Proteção contra brute force
Login, cadastro e reset de senha são alvos atraentes para credential stuffing. Várias camadas se aplicam:
- Bloqueio de conta. As configurações em Stores, Configuration, Customers, Customer Configuration, Password Options (máximo de falhas de login e tempo de bloqueio) também se aplicam ao
generateCustomerToken. - reCAPTCHA. Os módulos de reCAPTCHA do Magento podem proteger operações GraphQL como login de cliente, criação de conta e reset de senha. O frontend envia a resposta do reCAPTCHA em um header
X-ReCaptcha. O suporte a GraphQL é limitado a tipos específicos de reCAPTCHA, então confira a documentação da sua versão ao configurar em Stores, Configuration, Security, Google reCAPTCHA Storefront. - Rate limiting no servidor web ou na CDN para o endpoint
/graphql, por operação se o seu edge consegue inspecioná-la, ou ao menos por IP.
Queries de dados do cliente
Com um token, a query customer devolve o perfil, endereços, pedidos e mais. Peça só o que cada página precisa. A query de histórico de pedidos em particular pode ser pesada se você pedir todos os itens de todos os pedidos.
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 }
}
}
}
Queries de cliente nunca devem ser cacheadas por um cache compartilhado. Envie como POST ou sem opções de store para que nem o Varnish nem o seu frontend as cacheiem. Se você quer comportamento de cache para queries de catálogo feitas por clientes logados, veja o tratamento do X-Magento-Cache-Id em cache e performance do GraphQL.
Logando como cliente pelo admin
Times de suporte às vezes precisam ver o que o cliente vê. No Adobe Commerce, o recurso de remote shopping assistance oferece o generateCustomerTokenAsAdmin, que cria um token de cliente a partir de uma integração de admin quando o cliente permitiu a assistência. Essa mutation é só do Adobe Commerce. O Magento Open Source tem o recurso Login as Customer para a loja normal, mas um frontend headless precisa de tratamento próprio para ele.
Depois do login: o carrinho
Logo depois de um login bem-sucedido, leia o customerCart e faça o merge de qualquer carrinho de visitante nele com o mergeCarts. Pular isso é o motivo mais comum de clientes dizerem que os itens sumiram do carrinho quando fizeram login. O fluxo completo do carrinho está em mutations de carrinho e checkout no GraphQL.
FAQ
Quanto tempo dura um token de cliente do Magento?
Depende da configuração Customer Token Lifetime em Stores, Configuration, Services, OAuth, Access Token Expiration. O Magento não emite refresh tokens, então o cliente faz login de novo depois da expiração.
Onde uma loja headless deve guardar o token do cliente?
Em um cookie HTTP only e Secure definido pelo seu próprio servidor, que adiciona o header bearer ao chamar o Magento. Evite o localStorage, que qualquer script da página consegue ler.
Como detecto um token expirado no GraphQL?
O Magento devolve um erro com extensions.category igual a graphql-authorization. Capture de forma centralizada no seu cliente GraphQL, limpe o token e volte ao estado de visitante.
O generateCustomerTokenAsAdmin está disponível no Open Source?
Não. Ele pertence ao recurso de remote shopping assistance do Adobe Commerce.
Atendo como freelancer em Magento e Adobe Commerce. Se precisar de ajuda com autenticação headless, fale comigo.
Artigos relacionados
PWA Studio vs headless customizado no Magento: visão de dev
PWA Studio vs um frontend headless customizado no Magento: arquitetura, custos reais, lacunas do GraphQL, SEO, checkout e quando um theme server rendered vence.
Cache e performance do GraphQL no Magento 2: guia prático
Deixe o GraphQL do Magento 2 rápido: queries GET, cache no Varnish, X-Magento-Cache-Id, identities do @cache em resolvers customizados, batching de N+1 e limites de complexidade.
Checkout GraphQL no Magento 2: das mutations do carrinho ao pedido
Fluxo completo de checkout via GraphQL no Magento 2: carrinhos de visitante e cliente, addProductsToCart, endereços, frete, pagamento, placeOrder, merge de carrinhos e erros comuns.
GraphQL no Magento 2: adicione uma query e um resolver customizados
Como adicionar uma query GraphQL customizada no Magento 2.4: schema.graphqls, resolver com ResolverInterface, erros, checagem de auth e cache identity para o Varnish.