Clicks & Carts

Shopify app authentication: OAuth, session tokens and scopes

The install flow, the token you keep, and the session tokens embedded apps use instead of cookies — plus the mistakes that fail review.

8 min read · Apps & checkout ·

Shopify app authentication has two distinct halves that people routinely conflate. OAuth is how you get permission to act on a store — it happens once, at install. Session tokens are how your embedded admin UI proves who it's talking to on every request. Different mechanisms, different problems.

The install flow

  1. A merchant opens your app's install URL.
  2. You redirect them to Shopify's authorisation screen listing the scopes you're requesting.
  3. They approve. Shopify redirects back to your callback with a temporary code.
  4. You exchange that code for a permanent access token.
  5. You store the token against the shop domain and use it for API calls.

The token doesn't expire. It's revoked when the merchant uninstalls, which you learn about through the app/uninstalled webhook — and you must delete their data when you receive it.

The modern Remix template does all of this for you, which is a good argument for using it: the steps below are where hand-rolled implementations go wrong.

What you must verify

Three checks, all mandatory, all commonly missed:

HMAC on the OAuth callback. Shopify signs the query string. Verify it before exchanging the code, or you'll accept a forged callback.

The state parameter. Generate a nonce, send it, check it comes back. This is CSRF protection for the install flow.

The shop domain. Confirm it's a legitimate *.myshopify.com hostname before doing anything with it. Unvalidated shop parameters are an injection vector.

Miss any of these and the app either fails review or has a real vulnerability.

Session tokens in embedded apps

An embedded app renders in an iframe inside the Shopify admin. Third-party cookies in an iframe are unreliable and increasingly blocked, so embedded apps don't use cookie sessions.

Instead, App Bridge issues short-lived session tokens — JWTs signed with your app secret, valid for about a minute. The frontend attaches one to every request; your backend verifies the signature and reads which shop and user it represents.

Practically:

  • Verify the signature on every request. Never trust a shop parameter from the client.
  • They expire fast. Fetch a fresh one per request rather than caching.
  • Don't try to use cookies instead. It works in your browser and fails for a merchant with stricter settings, which is a support nightmare to diagnose.

Token exchange

Newer apps can obtain an access token by exchanging a session token directly, rather than sending the merchant through the redirect-based OAuth dance. It's faster, avoids a redirect out of the admin, and is what the current app template uses. If you're building new, use it; if you're maintaining an older app, the classic flow still works.

Scopes

Scopes are what you're allowed to touch. Declared in configuration:

``toml [access_scopes] scopes = "read_products,write_products,read_orders" ``

Three rules:

  1. Request the minimum. Merchants read this list. So does Shopify's review team, who will ask why a discount app wants customer data.
  2. Changing scopes requires re-authorisation. For a distributed app, that means every merchant re-approving. Get it right before you launch.
  3. Protected customer data needs justification. Access to customer PII requires an approval process and a stated reason. Don't request it speculatively.

Storing tokens

Access tokens are credentials for someone else's business.

  • Encrypt them at rest.
  • Never log them. Check your error handler isn't serialising the whole request object.
  • Keep them out of client-side code entirely.
  • Delete them when the merchant uninstalls.
  • Rotate your app's client secret if it's ever exposed.

The same discipline applies to any credentials the app holds for third-party systems — an ERP password in an environment variable is a credential you're responsible for.

Webhook verification is separate

Incoming webhooks are signed with an HMAC header, verified against your app secret. That's a different check from the OAuth HMAC and just as mandatory — an unverified webhook endpoint is an open door for anyone who guesses the URL. Webhooks covers it.

What fails review

  • Missing HMAC or state verification.
  • Requesting scopes the app clearly doesn't use.
  • Not implementing the mandatory privacy webhooks.
  • Storing tokens in plain text or logging them.
  • Cookie-based sessions in an embedded app.

All of these are on the list for App Store submission, and all are cheaper to do correctly than to retrofit after a rejection.

Authentication is the part of an app where "it works on my machine" is most misleading. It works until a merchant has stricter browser settings, and then it works for nobody.

Is this the problem you’re looking at?

Send me the link to your store and a line about what is going wrong. You get a straight answer within one business day — no pitch, no obligation.

[email protected]

Or see what I do around Shopify: services, work beyond the theme, selected work.

Keep reading

← All articles