Shopify-App-Authentifizierung
Shopify bietet mehrere Authentifizierungsabläufe, abhängig von Ihrem App-Typ und Anwendungsfall. Der moderne Standard ist der Token Exchange (2024+) für serverseitig gerenderte Apps, die Managed Installation für Headless-Apps und der OAuth 2.0 Authorization Code Grant für Legacy-/benutzerdefinierte Implementierungen. Alle Abläufe führen zu einem Zugriffstoken für die Shopify GraphQL Admin API.
Übersicht über Authentifizierungsabläufe
1. Token Exchange (Empfohlen ab 2024)
Anwendungsfall: Serverseitig gerenderte Apps (Remix, Next.js mit SSR), Shopify CLI Apps Ablauf: Händler installiert App → Shopify generiert temporäres Exchange-Token → App tauscht es gegen Zugriffstoken Sicherheit: Kein Client-Secret offengelegt; verwendet PKCE-ähnliche Rotation pro Anfrage Token-Lebensdauer: Zugriffstoken sind kurzlebig; Refresh-Token rotieren automatisch
Token Exchange Diagramm:
1. Händler klickt auf "Installieren" im Shopify-Admin
2. Shopify leitet weiter: https://ihre-app.com/auth/callback?code=EXCHANGE_TOKEN
3. App validiert HMAC, tauscht Code gegen Zugriffstoken (privat, nur serverseitig)
4. Shopify Admin API gewährt Bereiche; Token in Sitzung/Datenbank gespeichert
5. Token wird bei der nächsten Anfrage automatisch erneuert, falls abgelaufen
Remix-Implementierung (Token Exchange):
// shopify.app.ts (App-Konfiguration)
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, // Polaris-Admin-Dashboard
sessionStorage: new PrismaSessionStorage(prisma),
restResources, // Beinhaltet REST-API-Hilfsfunktionen
});
export default shopify;
Umgebungsvariablen (.env):
SHOPIFY_API_KEY=Ihr-öffentlicher-API-Schlüssel-aus-dem-Partner-Dashboard
SHOPIFY_API_SECRET=Ihr-privater-API-Secret
SHOPIFY_APP_URL=https://ihre-domain.ngrok.io # oder Produktions-URL
SCOPES=write_products,read_products,write_orders,read_orders
Auth-Routen (routes/auth.$.tsx - Catch-all-Route):
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 findet hier statt
};
Callback-Behandlung (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);
// Session enthält:
// - session.accessToken (gültig für API-Aufrufe)
// - session.shop (Shop-Domain des Händlers)
// - session.scope (gewährte Bereiche)
// - session.state (optionale benutzerdefinierte Daten)
return redirect('/app'); // Nach Authentifizierung zum Dashboard umleiten
};
Token in API-Aufrufen verwenden (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);
// Methode 1: Shopifys admin-Helfer verwenden (empfohlen)
const response = await admin.graphql(`
query GetProducts {
products(first: 10) {
edges {
node {
id
title
handle
}
}
}
}
`);
// Methode 2: Manueller GraphQL-Aufruf
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 || [] });
};
Token-Aktualisierung (Automatisch): Token-Exchange-Tokens aktualisieren sich automatisch über die Shopify-Sitzungs-Middleware. Keine manuelle Aktualisierung erforderlich:
// Tokens werden bei jeder Anfrage transparent aktualisiert
const response = await admin.graphql(query); // Handhabt Aktualisierung intern
2. Managed Installation (Headless/Benutzerdefinierte Apps)
Anwendungsfall: Headless-Storefront, mobile Apps, Integrationen von Drittanbietern Ablauf: Händler autorisiert App → Shopify generiert dauerhaftes Zugriffstoken (keine Secret-Rotation) Token-Lebensdauer: Langlebig; keine Aktualisierung erforderlich Sicherheit: Token ist dauerhaft; sicher in Umgebungsvariable speichern
Managed Installation Einrichtung:
Im Shopify-Partner-Dashboard:
- App-Einstellungen > API-Anmeldeinformationen
- "Managed installation" unter Admin-API-Zugriffsbereiche auswählen
- Händler erteilt einmalig Berechtigung
- Zugriffstoken in Ihre Umgebung kopieren
# .env
SHOPIFY_ADMIN_ACCESS_TOKEN=<SHOPIFY_ADMIN_ACCESS_TOKEN>
SHOPIFY_SHOP_URL=example-shop.myshopify.com
Managed Installation Token verwenden:
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 || '',
},
}
);
// Token läuft nie ab; API jederzeit aufrufbar
const query = `query { products(first: 10) { edges { node { id title } } } }`;
const products = await client.request(query);
3. OAuth 2.0 Authorization Code Grant (Legacy, weiterhin unterstützt)
Anwendungsfall: Öffentliche Apps mit traditionellem OAuth-Ablauf Ablauf: Händler klickt auf "Installieren" → App leitet zu Shopify OAuth weiter → Händler autorisiert → App erhält Code → App tauscht Code gegen Token Token-Lebensdauer: Langlebiges Zugriffstoken (kein Ablauf, es sei denn, es wird widerrufen) Sicherheit: Client-Secret erforderlich; PKCE optional
OAuth-Ablaufdiagramm:
1. Händler besucht: https://ihre-app.com/auth
2. App leitet weiter zu: https://ihr-shop.myshopify.com/admin/oauth/authorize?client_id=KEY&scope=write_products&redirect_uri=https://ihre-app.com/auth/callback&state=ZUFALL
3. Händler autorisiert App im Shopify-Admin
4. Shopify leitet zurück: https://ihre-app.com/auth/callback?code=AUTORISIERUNGSCODE&hmac=SIGNATUR&state=ZUFALL&shop=ihr-shop.myshopify.com
5. App validiert HMAC und state
6. App tauscht Code gegen Token (serverseitig, mit Client-Secret)
7. App speichert Token in Datenbank
Express.js OAuth-Beispiel:
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://ihre-app.com/auth/callback';
const SCOPES = 'write_products,read_products';
// Schritt 1: Händler zum Shopify-OAuth weiterleiten
app.get('/auth', (req, res) => {
const shop = req.query.shop as string;
if (!shop || !shop.includes('.myshopify.com')) {
return res.status(400).send('Fehlender oder ungültiger shop-Parameter');
}
const state = crypto.randomBytes(16).toString('hex');
const nonce = crypto.randomBytes(16).toString('hex');
// State in Sitzung (oder Datenbank) für Validierung speichern
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());
});
// Schritt 2: OAuth-Callback verarbeiten
app.get('/auth/callback', async (req, res) => {
const { code, hmac, shop, state } = req.query;
// HMAC validieren
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('Nicht autorisierte Anfrage erkannt');
}
// State validieren
if (state !== req.session.state) {
return res.status(401).send('State-Stimmen nicht überein');
}
try {
// Code gegen Zugriffstoken eintauschen
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;
// Zugriffstoken speichern (in Datenbank, nicht Sitzung, für Persistenz)
await storeAccessToken(shop as string, access_token, scope);
// Zum App-Dashboard umleiten
res.redirect(`/app?shop=${shop}`);
} catch (error) {
console.error('Token-Austauschfehler:', error);
res.status(500).send('Authentifizierung fehlgeschlagen');
}
});
// Hilfsfunktion zum Speichern des Tokens
async function storeAccessToken(shop: string, token: string, scope: string) {
// In Datenbank speichern (Beispiel mit Mock-Speicher)
const db = {
shops: {} as Record<string, { token: string; scope: string }>,
};
db.shops[shop] = { token, scope };
}
app.listen(3000);
Sitzungsspeicher-Adapter
Zugriffstoken müssen dauerhaft gespeichert werden. Shopify stellt Adapter für gängige Speicher-Backends bereit:
Prisma (Empfohlen für Remix)
Schema (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])
}
Einrichtung (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,
});
In-Memory (Nur für Entwicklung)
import { MemorySessionStorage } from '@shopify/shopify-app-session-storage';
const sessionStorage = new MemorySessionStorage();
const shopify = shopifyApp({
// ...
sessionStorage, // ACHTUNG: Sitzungen gehen bei App-Neustart verloren; nur für Entwicklung
});
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,
});
Online- vs. Offline-Tokens
Online-Token:
- Bereich: Berechtigungen des aktuellen Benutzers (normalerweise Admin-Benutzer)
- Ablauf: 24 Stunden
- Anwendungsfall: Browserbasierte Aktionen (Polaris-Admin-Dashboard)
- Einschränkungen: Kann keine Hintergrundjobs ausführen; eingeschränkt, wenn Benutzer abgemeldet ist
Offline-Token:
- Bereich: Gewährte Bereiche der App
- Ablauf: Keiner; dauerhaft bis Widerruf
- Anwendungsfall: Hintergrundjobs, Webhooks, geplante Aufgaben
- Einschränkungen: Keine; standardmäßig für App-Operationen verwenden
Offline-Token im Token Exchange anfordern:
// shopify.app.ts
const shopify = shopifyApp({
// ...
auth: {
path: '/auth',
callbackPath: '/auth/callback',
},
});
// Remix fordert standardmäßig automatisch ein Offline-Token an
// Keine Aktion erforderlich; session.accessToken für API-Aufrufe verwenden
Offline-Token für Webhooks verwenden:
// 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 ist Offline-Token; hier gültig
console.log(`Webhook verarbeitet mit Token für Shop: ${session.shop}`);
},
webhooksConfig
);
App-Proxy-Authentifizierung
Anwendungsfall: Storefront (öffentlich zugängliche) Anfragen an das App-Backend Authentifizierung: HMAC-Signaturvalidierung (wie bei Webhooks) Ablauf: Storefront → Liquid-Proxy-Anfrage → App-Backend (validiert HMAC) → Antwort
App-Proxy einrichten (shopify.app.toml):
[[extensions]]
type = "app_proxy"
name = "Storefront API"
url = "/api/proxy"
subpath = "loyalty" # Anfragen an /apps/loyalty/* werden hierher geleitet
App-Proxy-Handler (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') || '';
// Erstelle die zu validierende Nachricht (alle Parameter außer HMAC)
const params = new URLSearchParams(url.searchParams);
params.delete('hmac');
const message = params.toString();
// HMAC validieren
const expectedHmac = crypto
.createHmac('sha256', process.env.SHOPIFY_API_SECRET || '')
.update(message, 'utf8')
.digest('hex');
if (hmac !== expectedHmac) {
return json({ error: 'Ungültige Anfrage' }, { status: 401 });
}
// Anfrage ist authentifiziert; verarbeite sie
return json({ message: 'Proxy-Anfrage erfolgreich' });
};
Fehlerbehebung
| Problem | Ursache | Lösung |
|---|---|---|
403 Forbidden | Ungültige/abgelaufene Zugriffstoken | Token mit Shopify-Sitzungs-Middleware aktualisieren |
Invalid HMAC | Falsch berechneter HMAC | Überprüfen Sie die Parameterreihenfolge (alphabetisch), Secret und Kodierung |
OAuth redirect_uri mismatch | Nicht übereinstimmende Weiterleitungs-URL | Stimmt die URL in der Partner-Dashboard-Konfiguration genau überein? |
Session not found | Sitzungsspeicher leer oder nicht konfiguriert | Prisma migrieren (npx prisma db push) oder Redis-Verbindung überprüfen |
Token exchange failed | Ungültiger Exchange-Code | Code nur einmalig und innerhalb von 10 Minuten verwenden |
Best Practices
- Token-Exchange für 2024+ Apps verwenden – Vermeiden Sie direkte OAuth-Implementierungen, es sei denn, Sie benötigen Legacy-Kompatibilität.
- Offline-Tokens standardmäßig – Verwenden Sie Offline-Tokens für Hintergrundvorgänge; Online-Tokens sind auf 24 Stunden und Benutzersitzungen beschränkt.
- Sitzungen sicher speichern – Verwenden Sie Prisma, Redis oder DynamoDB für die Produktion; niemals In-Memory für Produktion.
- HMAC immer validieren – Überprüfen Sie HMAC bei Callbacks und Webhooks, um CSRF- und Replay-Angriffe zu verhindern.
- Bereiche minimieren – Fordern Sie nur die Bereiche an, die Ihre App benötigt; minimieren Sie so die Exposition.
- Tokens rotieren – Token-Exchange rotiert automatisch; bei Legacy-OAuth implementieren Sie regelmäßige Token-Rotation.