Authentification d'Application Shopify
Shopify propose plusieurs flux d'authentification selon le type d'application et le cas d'usage. Le standard moderne est Token Exchange (2024+) pour les applications rendues côté serveur, Managed Installation pour les applications headless, et OAuth 2.0 Authorization Code Grant pour les implémentations legacy/personnalisées. Tous les flux aboutissent à un jeton d'accès pour l'API GraphQL Admin de Shopify.
Aperçu des Flux d'Authentification
1. Token Exchange (Recommandé pour 2024+)
Cas d'Usage : Applications rendues côté serveur (Remix, Next.js avec SSR), applications Shopify CLI Flux : Le commerçant installe l'application → Shopify génère un jeton d'échange temporaire → L'application l'échange contre un jeton d'accès Sécurité : Aucun secret client exposé ; utilise une rotation de type PKCE par requête Durée de Vie du Jeton : Les jetons d'accès sont de courte durée ; les jetons de rafraîchissement tournent automatiquement
Diagramme Token Exchange :
1. Le commerçant clique sur "Installer" dans l'Admin Shopify
2. Shopify redirige : https://votre-app.com/auth/callback?code=EXCHANGE_TOKEN
3. L'application valide le HMAC, échange le code contre un jeton d'accès (côté serveur privé uniquement)
4. L'API Admin Shopify accorde les portées ; le jeton stocké en session/base de données
5. Le jeton s'auto-rafraîchit à la prochaine requête s'il est expiré
Implémentation Remix (Token Exchange) :
// shopify.app.ts (Configuration de l'application)
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, // Tableau de bord Polaris
sessionStorage: new PrismaSessionStorage(prisma),
restResources, // Inclut les aides de l'API REST
});
export default shopify;
Variables d'Environnement (.env) :
SHOPIFY_API_KEY=votre-cle-api-publique-du-tableau-de-bord-partenaire
SHOPIFY_API_SECRET=votre-secret-api-prive
SHOPIFY_APP_URL=https://votre-domaine.ngrok.io # ou URL de production
SCOPES=write_products,read_products,write_orders,read_orders
Routes d'Authentification (routes/auth.$.tsx - Route catch-all) :
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); // Token Exchange s'effectue ici
};
Gestion du 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);
// La session contient :
// - session.accessToken (valide pour les appels API)
// - session.shop (domaine de la boutique du commerçant)
// - session.scope (portées accordées)
// - session.state (données personnalisées optionnelles)
return redirect('/app'); // Rediriger vers le tableau de bord après authentification
};
Utilisation du Jeton dans les Appels 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éthode 1 : Utiliser l'aide admin de Shopify (recommandé)
const response = await admin.graphql(`
query GetProducts {
products(first: 10) {
edges {
node {
id
title
handle
}
}
}
}
`);
// Méthode 2 : Appel GraphQL manuel
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 || [] });
};
Rafraîchissement du Jeton (Automatique) : Les jetons Token Exchange se rafraîchissent automatiquement via le middleware de session de Shopify. Aucun rafraîchissement manuel nécessaire :
// Les jetons sont rafraîchis de manière transparente à chaque requête
const response = await admin.graphql(query); // Gère le rafraîchissement en interne
2. Managed Installation (Applications Headless/Personnalisées)
Cas d'Usage : Storefront headless, applications mobiles, intégrations tierces Flux : Le commerçant autorise l'application → Shopify génère un jeton d'accès permanent (pas de rotation de secret) Durée de Vie du Jeton : Longue durée ; aucun rafraîchissement requis Sécurité : Le jeton est permanent ; à stocker en toute sécurité dans une variable d'environnement
Configuration Managed Installation :
Dans le Tableau de Bord Partenaire Shopify :
- Paramètres de l'application > Identifiants API
- Sélectionner "Managed installation" sous les portées d'accès à l'API Admin
- Le commerçant accorde l'autorisation une fois
- Copier le jeton d'accès dans votre environnement
# .env
SHOPIFY_ADMIN_ACCESS_TOKEN=<SHOPIFY_ADMIN_ACCESS_TOKEN>
SHOPIFY_SHOP_URL=exemple-boutique.myshopify.com
Utilisation du Jeton Managed Installation :
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 || '',
},
}
);
// Le jeton n'expire jamais ; appelez l'API à tout moment
const query = `query { products(first: 10) { edges { node { id title } } } }`;
const products = await client.request(query);
3. OAuth 2.0 Authorization Code Grant (Legacy, Toujours Supporté)
Cas d'Usage : Applications publiques avec flux OAuth traditionnel Flux : Le commerçant clique sur "Installer" → L'application redirige vers Shopify OAuth → Le commerçant autorise → L'application reçoit un code → L'application échange le code contre un jeton Durée de Vie du Jeton : Jeton d'accès longue durée (pas d'expiration sauf révocation) Sécurité : Secret client requis ; PKCE optionnel
Diagramme du Flux OAuth :
1. Le commerçant visite : https://votre-app.com/auth
2. L'application redirige vers : https://votre-boutique.myshopify.com/admin/oauth/authorize?client_id=KEY&scope=write_products&redirect_uri=https://votre-app.com/auth/callback&state=RANDOM
3. Le commerçant autorise l'application dans l'Admin Shopify
4. Shopify redirige vers : https://votre-app.com/auth/callback?code=AUTHORIZATION_CODE&hmac=SIGNATURE&state=RANDOM&shop=votre-boutique.myshopify.com
5. L'application valide le HMAC et l'état
6. L'application échange le code contre un jeton (côté serveur, en utilisant le secret client)
7. L'application stocke le jeton dans la base de données
Exemple Express.js OAuth :
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://votre-app.com/auth/callback';
const SCOPES = 'write_products,read_products';
// Étape 1 : Rediriger le commerçant vers Shopify OAuth
app.get('/auth', (req, res) => {
const shop = req.query.shop as string;
if (!shop || !shop.includes('.myshopify.com')) {
return res.status(400).send('Paramètre shop manquant ou invalide');
}
const state = crypto.randomBytes(16).toString('hex');
const nonce = crypto.randomBytes(16).toString('hex');
// Stocker l'état dans la session (ou base de données) pour validation
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());
});
// Étape 2 : Gérer le callback OAuth
app.get('/auth/callback', async (req, res) => {
const { code, hmac, shop, state } = req.query;
// Valider le 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('Requête non autorisée détectée');
}
// Valider l'état
if (state !== req.session.state) {
return res.status(401).send('Non-concordance de l\'état');
}
try {
// Échanger le code contre un jeton d'accès
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;
// Stocker le jeton d'accès (dans la base de données, pas la session, pour persistance)
await storeAccessToken(shop as string, access_token, scope);
// Rediriger vers le tableau de bord de l'application
res.redirect(`/app?shop=${shop}`);
} catch (error) {
console.error('Erreur d\'échange de jeton :', error);
res.status(500).send('Authentification échouée');
}
});
// Fonction d'aide pour stocker le jeton
async function storeAccessToken(shop: string, token: string, scope: string) {
// Stocker dans la base de données (exemple avec stockage mock)
const db = {
shops: {} as Record<string, { token: string; scope: string }>,
};
db.shops[shop] = { token, scope };
}
app.listen(3000);
Adaptateurs de Stockage de Session
Les jetons d'accès doivent être stockés de manière persistante. Shopify fournit des adaptateurs pour les backends de stockage courants :
Prisma (Recommandé pour Remix)
Schéma (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])
}
Configuration (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,
});
En Mémoire (Développement Uniquement)
import { MemorySessionStorage } from '@shopify/shopify-app-session-storage';
const sessionStorage = new MemorySessionStorage();
const shopify = shopifyApp({
// ...
sessionStorage, // ATTENTION : Sessions perdues au redémarrage de l'application ; développement uniquement
});
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,
});
Jetons en Ligne vs. Hors Ligne
Jeton en Ligne :
- Portée : Autorisations de l'utilisateur actuel (généralement admin)
- Expiration : 24 heures
- Cas d'Usage : Actions basées sur le navigateur (tableau de bord Polaris)
- Limitations : Ne peut pas exécuter de tâches en arrière-plan ; limité lorsque l'utilisateur se déconnecte
Jeton Hors Ligne :
- Portée : Portées accordées à l'application
- Expiration : Aucune ; permanent jusqu'à révocation
- Cas d'Usage : Tâches en arrière-plan, webhooks, tâches planifiées
- Limitations : Aucune ; à utiliser par défaut pour les opérations de l'application
Demande de Jeton Hors Ligne dans Token Exchange :
// shopify.app.ts
const shopify = shopifyApp({
// ...
auth: {
path: '/auth',
callbackPath: '/auth/callback',
},
});
// Remix demande automatiquement le jeton hors ligne par défaut
// Aucune action nécessaire ; utilisez session.accessToken pour les appels API
Utilisation du Jeton Hors Ligne pour les 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 est le jeton hors ligne ; valide ici
console.log(`Webhook traité avec le jeton pour la boutique : ${session.shop}`);
},
webhooksConfig
);
Authentification par Proxy d'Application
Cas d'Usage : Requêtes Storefront (publiques) vers le backend de l'application Authentification : Validation de signature HMAC (comme les webhooks) Flux : Storefront → Requête proxy Liquid → Backend de l'application (valide HMAC) → Réponse
Configuration du Proxy d'Application (shopify.app.toml) :
[[extensions]]
type = "app_proxy"
name = "API Storefront"
url = "/api/proxy"
subpath = "loyalty" # Les requêtes vers /apps/loyalty/* sont routées ici
Gestionnaire de Proxy d'Application (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') || '';
// Construction et validation HMAC...
// ...
return json({ data: 'Réponse du proxy d\'application' });
};