Authenticate every request
Authenticate every request before you read or write store data, and take the store from the result of that authentication, never from the request itself.
Your shipped JavaScript, including theme app extension assets and embedded app bundles, names every endpoint that it calls, so every endpoint is discoverable. An endpoint that accepts the store from a shop parameter or body field lets any caller act as any store that has your app installed. Endpoints that save settings your storefront code renders, such as widget settings or custom CSS, JavaScript, or HTML, are the highest-impact case, because one request puts a script on a storefront.
Expiring access tokens and API log reviews don't detect this. Your own server makes the follow-up GraphQL Admin API call with a valid token, so the request looks like normal traffic.
Match the authentication method to where the request comes from, then reject anything that doesn't verify.
Anchor to Match the method to the requestMatch the method to the request
| Request comes from | Authenticate it with | In the React Router app template |
|---|---|---|
| Your embedded app in the Shopify admin | ID token | authenticate.admin |
| A storefront, including JavaScript in your theme app extension | App proxy signature | authenticate.public.appProxy |
| A checkout or customer account UI extension | The extension's session token | authenticate.public.checkout or authenticate.public.customerAccount |
| A Shopify webhook | HMAC signature | authenticate.webhook |
Anchor to Reject what doesn't verifyReject what doesn't verify
- If a request has no valid token or signature, then reject it before doing anything else. Don't fall back to a
shopvalue from a parameter, body field, header, or cookie. - An app proxy signature proves which store a request came through, not who sent it, so don't let app proxy routes change store settings. For actions on behalf of a logged-in customer, use the
logged_in_customer_idvalue from the signed request. - A request that your storefront JavaScript sends directly to your server, rather than through an app proxy, carries no proof of which store it came from. Treat it as anonymous: it can read public data, but it must not change anything for a store.
- Debug, test, migration, and legacy routes need the same authentication. If you don't need a route, then remove it.
Anchor to If you don't use a Shopify libraryIf you don't use a Shopify library
- Validate every part of the ID token: signature, expiry, audience, and destination claims.
- Verify webhook HMACs against the raw request body before parsing, and treat webhook headers, including the topic and shop domain, as untrusted until the signature verifies. Make webhook handling idempotent, and use delivery IDs to detect replays.
- Bind the OAuth
statevalue to the browser that started the flow, and check it on return. Require exact matches against your approved redirect URLs. - Validate a shop domain against a pattern that's anchored at both ends, so that a value such as
example.myshopify.com.attacker.netdoesn't match.
For details, see authentication and authorization and verify webhook deliveries.