← Blog

Token Exchange killed the OAuth redirect

Most Shopify dev tutorials still teach OAuth redirect because Shopify hasn't deleted those docs. They should. Here's what the 2025 Token Exchange + Managed Installation migration actually looks like — what to delete, what to write, and the gotchas nobody warns you about.

There’s a tab open in your browser right now that loads https://your-app.com/auth?shop=...&hmac=...&timestamp=.... If you wrote that route after January 2025, you wrote dead code.

The new model — Managed Installation plus Token Exchange — has been the recommended path for embedded Shopify apps for almost a year. Most tutorials still teach the redirect flow because Shopify hasn’t taken the old docs down. They should. The migration is shorter than people think, and the result is simpler in every direction.

What changed

Managed Installation moved the install + consent screen into Shopify itself. You declare the scopes your app needs in shopify.app.toml, push it with shopify app deploy, and Shopify handles the install UX. No redirect. No state nonce. No HMAC-on-querystring dance.

App Bridge — the JS SDK that runs inside the Admin iframe — mints a session token (a JWT) on every request from the embedded UI. Your backend takes that JWT and exchanges it for an offline access token via the Token Exchange endpoint. The offline token is what you use for every subsequent Shopify API call.

End state: no /auth route, no /auth/callback, no nonces, no install-flow cookies, no post-install redirect logic. Just middleware.

What you delete

  • The /auth install URL builder
  • The /auth/callback handler
  • The HMAC verifier on the redirect query string (you still need HMAC on webhooks — don’t delete that one)
  • The cookie-based pre-auth session
  • The post-install redirect URL builder
  • The state nonce store

Most of this lives in one or two files. Deleting it is satisfying.

What you write instead

Middleware. About 80 lines of Go. The shape:

  1. Pull the JWT out of the Authorization: Bearer … header.
  2. Validate the signature against your app secret.
  3. Verify the audience and issuer claims — both must match the shop domain making the request.
  4. Look up an offline token for that shop in Redis.
  5. If miss: call Token Exchange with the JWT as subject_token, cache the returned offline token under {app}:token:{shop_domain}.
  6. Hand the offline token to the request context.

That’s it. One round-trip per uncached shop, then everything is local.

Gotchas nobody warns you about

Session tokens expire after 60 seconds. App Bridge auto-refreshes them on the client. Server-side webhook handlers don’t have App Bridge. They still need HMAC verification — do not delete that path.

invalid_subject_token is a soft error. Token Exchange returns it when a shop has uninstalled mid-flight, or when the JWT is past its 60-second window. Treat it as a 401 and let the client retry. Treating it as a 500 will fill your error tracker with noise during normal uninstalls.

The offline token can be revoked out-of-band. A merchant can uninstall and reinstall in the same minute. Your cache won’t know. The right invalidation trigger is “next failed API call returns invalid_token” — not the install webhook, which can race.

Local dev still uses the live Shopify issuer. Even when you’re tunneling to localhost, the JWT issuer is the production Shopify domain. Don’t hardcode issuer checks against your tunnel URL.

When NOT to migrate

If your app is non-embedded — lives outside the Admin iframe and serves its own dashboard — OAuth redirect is still the right tool. App Bridge isn’t there to mint session tokens, so there’s nothing to exchange. Our event pipeline app stays on OAuth redirect for exactly this reason. Embedded apps are the ones that benefit.


Recurrabee and searchabee both run this pattern in production today — see /work for what they actually do. The migration took a couple of afternoons each. The auth code that’s left is shorter, easier to reason about, and stops being something you have to think about.