Autenticação de Aplicativos Shopify
O Shopify fornece múltiplos fluxos de autenticação dependendo do tipo de aplicativo e caso de uso. O padrão moderno é a Troca de Token (2024+) para aplicativos renderizados no servidor, a Instalação Gerenciada para aplicativos headless e o OAuth 2.0 Authorization Code Grant para implementações legadas/personalizadas. Todos os fluxos resultam em um token de acesso para a Shopify GraphQL Admin API.
Visão Geral dos Fluxos de Autenticação
1. Troca de Token (Recomendado para 2024+)
Caso de Uso: Aplicativos renderizados no servidor (Remix, Next.js com SSR), aplicativos Shopify CLI Fluxo: Comerciante instala o aplicativo → Shopify gera um token de troca temporário → O aplicativo troca por um token de acesso Segurança: Nenhum segredo de cliente exposto; usa rotação estilo PKCE por requisição Tempo de Vida do Token: Tokens de acesso são de curta duração; tokens de atualização giram automaticamente
Diagrama de Troca de Token:
1. O comerciante clica em "Instalar" no Admin Shopify
2. Shopify redireciona: https://seu-aplicativo.com/auth/callback?code=EXCHANGE_TOKEN
3. O aplicativo valida o HMAC, troca o código por token de acesso (privado, apenas no servidor)
4. A Shopify Admin API concede escopos; token armazenado na sessão/banco de dados
5. O token é atualizado automaticamente na próxima requisição se expirar
Implementação Remix (Troca de Token):
// shopify.app.ts (Configuração do Aplicativo)
import { shopifyApp } from '@shopify/shopify-app-remix/server';
import { restResources } from '@shopify/shopify-api/rest/admin/2026-07';
import { PrismaSessionStorage } from '@shopify/shopify-app-session-storage-prisma';
import { prisma } from '~/db.server';
const shopify = shopifyApp({
apiKey: process.env.SHOPIFY_API_KEY || '',
apiSecret: process.env.SHOPIFY_API_SECRET || '',
scopes: process.env.SCOPES?.split(',') || [
'write_products',
'read_products',
'write_orders',
'read_orders',
'write_customers',
'read_customers',
'write_discounts',
'read_discounts',
'write_fulfillments',
'read_fulfillments',
'write_inventory',
'read_inventory',
],
appUrl: process.env.SHOPIFY_APP_URL || 'http://localhost:3000',
auth: {
path: '/auth',
callbackPath: '/auth/callback',
},
webhooks: {
path: '/webhooks',
},
isEmbeddedApp: true, // Painel de administração Polaris
sessionStorage: new PrismaSessionStorage(prisma),
restResources, // Inclui auxiliares da REST API
});
export default shopify;
Variáveis de Ambiente (.env):
SHOPIFY_API_KEY=sua-chave-publica-da-api-do-painel-de-parceiros
SHOPIFY_API_SECRET=seu-segredo-privado-da-api
SHOPIFY_APP_URL=https://seu-dominio.ngrok.io # ou URL de produção
SCOPES=write_products,read_products,write_orders,read_orders
Rotas de Autenticação (routes/auth.$.tsx - Rota curinga):
import { redirect } from '@shopify/remix-oxygen';
import { authenticate } from '~/shopify.server';
export const loader = async ({ request }) => {
const { authenticate } = await import('~/shopify.server');
return authenticate.admin(request); // A Troca de Token acontece aqui
};
Manipulação de Callback (routes/auth.callback.tsx):
import { json } from '@shopify/remix-oxygen';
import { authenticate } from '~/shopify.server';
export const loader = async ({ request }) => {
const { session } = await authenticate.admin(request);
// A sessão contém:
// - session.accessToken (válido para chamadas de API)
// - session.shop (domínio da loja do comerciante)
// - session.scope (escopos concedidos)
// - session.state (dados personalizados opcionais)
return redirect('/app'); // Redireciona para o painel após autenticação
};
Usando o Token em Chamadas de API (routes/app.products.tsx):
import { json } from '@shopify/remix-oxygen';
import { authenticate } from '~/shopify.server';
import { GraphQLClient } from 'graphql-request';
export const loader = async ({ request }) => {
const { session, admin } = await authenticate.admin(request);
// Método 1: Usar o auxiliar admin do Shopify (recomendado)
const response = await admin.graphql(`
query GetProducts {
products(first: 10) {
edges {
node {
id
title
handle
}
}
}
}
`);
// Método 2: Chamada GraphQL manual
const client = new GraphQLClient(
`https://${session.shop}/admin/api/2026-07/graphql.json`,
{
headers: {
'X-Shopify-Access-Token': session.accessToken,
'Content-Type': 'application/json',
},
}
);
const data = await client.request(/* ... */);
return json({ products: response.data?.products?.edges || [] });
};
Atualização de Token (Automática): Os tokens da Troca de Token são atualizados automaticamente pelo middleware de sessão do Shopify. Nenhuma atualização manual é necessária:
// Os tokens são atualizados de forma transparente em cada requisição
const response = await admin.graphql(query); // Lida com a atualização internamente
2. Instalação Gerenciada (Aplicativos Headless / Personalizados)
Caso de Uso: Loja virtual headless, aplicativos móveis, integrações de terceiros Fluxo: Comerciante autoriza o aplicativo → Shopify gera um token de acesso permanente (sem rotação de segredo) Tempo de Vida do Token: Longa duração; nenhuma atualização necessária Segurança: O token é permanente; armazene com segurança em variável de ambiente
Configuração da Instalação Gerenciada:
No Painel de Parceiros Shopify:
- Configurações do Aplicativo > Credenciais da API
- Selecione "Instalação gerenciada" nos escopos de acesso da API Admin
- O comerciante concede permissão uma vez
- Copie o token de acesso para seu ambiente
# .env
SHOPIFY_ADMIN_ACCESS_TOKEN=<SHOPIFY_ADMIN_ACCESS_TOKEN>
SHOPIFY_SHOP_URL=exemplo-loja.myshopify.com
Usando o Token de Instalação Gerenciada:
import { GraphQLClient } from 'graphql-request';
const client = new GraphQLClient(
`https://${process.env.SHOPIFY_SHOP_URL}/admin/api/2026-07/graphql.json`,
{
headers: {
'X-Shopify-Access-Token': process.env.SHOPIFY_ADMIN_ACCESS_TOKEN || '',
},
}
);
// O token nunca expira; chame a API a qualquer momento
const query = `query { products(first: 10) { edges { node { id title } } } }`;
const products = await client.request(query);
3. OAuth 2.0 Authorization Code Grant (Legado, Ainda Suportado)
Caso de Uso: Aplicativos públicos com fluxo OAuth tradicional Fluxo: Comerciante clica em "Instalar" → O aplicativo redireciona para o OAuth do Shopify → Comerciante autoriza → O aplicativo recebe o código → O aplicativo troca o código pelo token Tempo de Vida do Token: Token de acesso de longa duração (sem expiração, a menos que revogado) Segurança: Segredo do cliente necessário; PKCE opcional
Diagrama do Fluxo OAuth:
1. O comerciante visita: https://seu-aplicativo.com/auth
2. O aplicativo redireciona para: https://sua-loja.myshopify.com/admin/oauth/authorize?client_id=KEY&scope=write_products&redirect_uri=https://seu-aplicativo.com/auth/callback&state=RANDOM
3. O comerciante autoriza o aplicativo no Admin Shopify
4. Shopify redireciona de volta: https://seu-aplicativo.com/auth/callback?code=AUTHORIZATION_CODE&hmac=SIGNATURE&state=RANDOM&shop=sua-loja.myshopify.com
5. O aplicativo valida o HMAC e o estado
6. O aplicativo troca o código pelo token (no servidor, usando o segredo do cliente)
7. O aplicativo armazena o token no banco de dados
Exemplo OAuth com Express.js:
import express from 'express';
import axios from 'axios';
import crypto from 'crypto';
const app = express();
const API_KEY = process.env.SHOPIFY_API_KEY || '';
const API_SECRET = process.env.SHOPIFY_API_SECRET || '';
const REDIRECT_URI = process.env.REDIRECT_URI || 'https://seu-aplicativo.com/auth/callback';
const SCOPES = 'write_products,read_products';
// Etapa 1: Redirecionar o comerciante para o OAuth do Shopify
app.get('/auth', (req, res) => {
const shop = req.query.shop as string;
if (!shop || !shop.includes('.myshopify.com')) {
return res.status(400).send('Parâmetro shop ausente ou inválido');
}
const state = crypto.randomBytes(16).toString('hex');
const nonce = crypto.randomBytes(16).toString('hex');
// Armazenar o estado na sessão (ou banco de dados) para validação
req.session.state = state;
req.session.nonce = nonce;
const authUrl = new URL(
`/admin/oauth/authorize`,
`https://${shop}`
);
authUrl.searchParams.append('client_id', API_KEY);
authUrl.searchParams.append('scope', SCOPES);
authUrl.searchParams.append('redirect_uri', REDIRECT_URI);
authUrl.searchParams.append('state', state);
res.redirect(authUrl.toString());
});
// Etapa 2: Lidar com o callback OAuth
app.get('/auth/callback', async (req, res) => {
const { code, hmac, shop, state } = req.query;
// Validar HMAC
const message = Object.entries(req.query)
.filter(([key]) => key !== 'hmac')
.map(([key, value]) => `${key}=${value}`)
.sort()
.join('&');
const hash = crypto
.createHmac('sha256', API_SECRET)
.update(message, 'utf8')
.digest('base64');
if (hash !== hmac) {
return res.status(401).send('Requisição não autorizada detectada');
}
// Validar estado
if (state !== req.session.state) {
return res.status(401).send('Incompatibilidade de estado');
}
try {
// Trocar o código pelo token de acesso
const response = await axios.post(
`https://${shop}/admin/oauth/access_token`,
{
client_id: API_KEY,
client_secret: API_SECRET,
code,
}
);
const { access_token, scope } = response.data;
// Armazenar o token de acesso (no banco de dados, não na sessão, para persistência)
await storeAccessToken(shop as string, access_token, scope);
// Redirecionar para o painel do aplicativo
res.redirect(`/app?shop=${shop}`);
} catch (error) {
console.error('Erro na troca de token:', error);
res.status(500).send('Autenticação falhou');
}
});
// Função auxiliar para armazenar o token
async function storeAccessToken(shop: string, token: string, scope: string) {
// Armazenar no banco de dados (exemplo usando armazenamento fictício)
const db = {
shops: {} as Record<string, { token: string; scope: string }>,
};
db.shops[shop] = { token, scope };
}
app.listen(3000);
Adaptadores de Armazenamento de Sessão
Os tokens de acesso devem ser armazenados de forma persistente. O Shopify fornece adaptadores para backends de armazenamento comuns:
Prisma (Recomendado para Remix)
Esquema (prisma/schema.prisma):
model Session {
id String @id
shop String
state String
isOnline Boolean @default(false)
accessToken String
refreshToken String?
scope String
expiresAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([shop, state])
@@index([shop])
}
Configuração (shopify.app.ts):
import { PrismaSessionStorage } from '@shopify/shopify-app-session-storage-prisma';
import { prisma } from '~/db.server';
const shopify = shopifyApp({
// ...
sessionStorage: new PrismaSessionStorage(prisma),
});
Redis
import { RedisSessionStorage } from '@shopify/shopify-app-session-storage-redis';
import redis from 'redis';
const redisClient = redis.createClient({
host: 'localhost',
port: 6379,
});
const sessionStorage = new RedisSessionStorage({
client: redisClient,
prefix: 'shopify_session:',
});
const shopify = shopifyApp({
// ...
sessionStorage,
});
Em Memória (Apenas Desenvolvimento)
import { MemorySessionStorage } from '@shopify/shopify-app-session-storage';
const sessionStorage = new MemorySessionStorage();
const shopify = shopifyApp({
// ...
sessionStorage, // ATENÇÃO: Sessões perdidas na reinicialização do aplicativo; apenas desenvolvimento
});
DynamoDB
import { DynamoDBSessionStorage } from '@shopify/shopify-app-session-storage-dynamodb';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
const dynamoDBClient = new DynamoDBClient({ region: 'us-east-1' });
const sessionStorage = new DynamoDBSessionStorage({
client: dynamoDBClient,
tableName: 'shopify-sessions',
});
const shopify = shopifyApp({
// ...
sessionStorage,
});
Tokens Online vs. Offline
Token Online:
- Escopo: Permissões do usuário atual (tipicamente usuário administrador)
- Expiração: 24 horas
- Caso de Uso: Ações baseadas em navegador (painel de administração Polaris)
- Limitações: Não pode executar tarefas em segundo plano; limitado quando o usuário faz logout
Token Offline:
- Escopo: Escopos concedidos ao aplicativo
- Expiração: Nenhuma; permanente até ser revogado
- Caso de Uso: Tarefas em segundo plano, webhooks, tarefas agendadas
- Limitações: Nenhuma; use por padrão para operações do aplicativo
Solicitando Token Offline na Troca de Token:
// shopify.app.ts
const shopify = shopifyApp({
// ...
auth: {
path: '/auth',
callbackPath: '/auth/callback',
},
});
// Remix solicita automaticamente o token offline por padrão
// Nenhuma ação necessária; use session.accessToken para chamadas de API
Usando Token Offline para Webhooks:
// webhooks/products-update.ts
export const webhooks = {
APP_UNINSTALLED: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: '/webhooks/app-uninstalled',
},
PRODUCTS_UPDATE: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: '/webhooks/products-update',
},
};
export default defineWebhooksConfig(
async (request, { admin, session }) => {
const { body, query } = await graphql.query(request, {
query: GET_PRODUCT,
variables: { id: 'gid://shopify/Product/123' },
});
// session.accessToken é um token offline; válido aqui
console.log(`Webhook processado com token para a loja: ${session.shop}`);
},
webhooksConfig
);
Autenticação de Proxy de Aplicativo
Caso de Uso: Requisições de loja virtual (públicas) para o backend do aplicativo Autenticação: Validação de assinatura HMAC (como webhooks) Fluxo: Loja virtual → Requisição de proxy Liquid → Backend do aplicativo (valida HMAC) → Resposta
Configurando Proxy de Aplicativo (shopify.app.toml):
[[extensions]]
type = "app_proxy"
name = "API da Loja Virtual"
url = "/api/proxy"
subpath = "lealdade" # Requisições para /apps/lealdade/* são roteadas para aqui
Manipulador de Proxy de Aplicativo (Remix routes/api/proxy.ts):
import { json } from '@shopify/remix-oxygen';
import crypto from 'crypto';
export const loader = async ({ request }) => {
const url = new URL(request.url);
const hmac = url.searchParams.get('hmac') || '';
const timestamp = url.searchParams.get('_t') || '';
const shop = url.searchParams.get('shop') || '';
// Construir string de mensagem para validação HMAC
const params = new URLSearchParams(url.searchParams);
params.delete('hmac');
params.sort();
const message = params.toString();
// Calcular HMAC esperado
const hash = crypto
.createHmac('sha256', process.env.SHOPIFY_API_SECRET || '')
.update(message)
.digest('hex');
if (hash !== hmac) {
return json({ error: 'HMAC inválido' }, { status: 401 });
}
// Processar a requisição do proxy (exemplo: buscar dados de fidelidade)
const products = await shopify.api.rest.Product.all({
session: { shop, accessToken: 'offline-token-storage' },
});
// Retornar dados renderizados em Liquid ou JSON
return json({ products: products.data });
};
Próximos Passos
- Escolha seu fluxo de autenticação (Troca de Token para novos projetos)
- Configure o adaptador de armazenamento de sessão (Prisma para Remix)
- Implemente rotas de autenticação e callback
- Use tokens para chamar a Shopify Admin API
- Implante com Shopify CLI ou sua plataforma de hospedagem
💡 Dica: Para novos projetos, sempre use a Troca de Token com tokens offline. Evite OAuth legado, a menos que tenha requisitos específicos de compatibilidade.