Skip to content

feat(shopify): OAuth (offline token) via a store-domain prompt - #526

Merged
guitavano merged 3 commits into
mainfrom
guitavano/shopify-mcp-token
Jul 28, 2026
Merged

feat(shopify): OAuth (offline token) via a store-domain prompt#526
guitavano merged 3 commits into
mainfrom
guitavano/shopify-mcp-token

Conversation

@guitavano

@guitavano guitavano commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

What

Replaces the Shopify MCP's manual custom-app token auth with a one-click OAuth grant (authorization code, offline token).

Why it needed a custom flow

Shopify's authorize endpoint is per-store (https://{shop}.myshopify.com/admin/oauth/authorize), but the runtime's authorizationUrl(callbackUrl) hook doesn't know which store the user wants — so we can't build it up front. Following the WhatsApp MCP pattern, the hook points at our own /oauth/custom page, which asks the merchant for their shop domain and then drives the real Shopify grant, handing the mesh a sealed credential.

Flow

  1. runtime → authorizationUrl → 302 to /oauth/custom?callback_url=…
  2. /oauth/custom renders a store-domain form → 302 to Shopify /admin/oauth/authorize (redirect_uri back to us, mesh callback signed into state)
  3. Shopify → /oauth/shopify/callback → verify HMAC + state, exchange code for an offline token, seal {shop, token}, 302 back to the mesh callback with it as code
  4. runtime → exchangeCode({ code }) → validate and return the sealed credential as the connection's access token

Changes

  • server/lib/token.ts — stateless crypto: HMAC-signed state + AES-256-GCM sealed {shop, token} credential (Admin token never travels readable through browser redirects) + Shopify callback HMAC verification.
  • server/lib/oauth.ts — the oauth hook and the two custom route handlers. Offline token → no refreshToken. Requested scopes trimmed to what a standard store can grant (Plus/Payments-gated scopes excluded) and overridable via SHOPIFY_SCOPES.
  • server/lib/client.tsresolveCredentials opens the sealed credential (both shop + token come from it). Legacy raw-token + env-var dev paths kept for back-compat.
  • types/env.ts + app.json — drop the storeDomain config field and the token auth block; refresh descriptions.
  • scripts/deploy.ts / deploy.yml / SECRETS.md — wire SHOPIFY_CLIENT_ID, SHOPIFY_CLIENT_SECRET, SHOPIFY_TOKEN_SECRET, SHOPIFY_SCOPES.
  • README.md — rewrite setup + auth notes.

Deploy prerequisites (not code)

  • A public Shopify Partner app with App URL https://sites-shopify.deco.site and redirect https://sites-shopify.deco.site/oauth/shopify/callback.
  • Secrets: SHOPIFY_CLIENT_ID, SHOPIFY_CLIENT_SECRET, SHOPIFY_TOKEN_SECRET (openssl rand -hex 32).
  • Assumes the deco platform injects SELF_URL/MESH_URL at runtime (same as the WhatsApp MCP) — needs confirming in staging.

Testing

  • bun test51 pass (token sign/seal round-trips, tamper detection, Shopify HMAC, both OAuth route handlers, credential resolution incl. sealed + legacy paths, scope defaults + env override).
  • tsc --noEmit clean for project files.
  • ⚠️ The full end-to-end OAuth loop (runtime → Shopify → mesh callback) can't be fully exercised without the mesh; validate on a real store after deploy.

🤖 Generated with Claude Code


Summary by cubic

Replaces manual token auth with a store‑domain prompted OAuth flow that issues a read‑only offline token and seals {shop, token} as the connection credential. Drops all connection config (storeDomain, apiVersion), updates registry.json copy, and keeps legacy token paths for dev/back‑compat.

  • New Features

    • Per‑store OAuth via /oauth/custom + Shopify callback; offline token; runtime wired with the oauth hook.
    • Sealed credential (AES‑256‑GCM), signed state, and Shopify HMAC validation; token never appears in redirects.
    • Client opens the sealed credential for shop + token; falls back to legacy raw token + state or env; default scopes trimmed and overridable via SHOPIFY_SCOPES.
    • Admin API version defaults to 2026-07; override per deploy with SHOPIFY_API_VERSION.
  • Migration

    • Requires a public Shopify Partner app and secrets: SHOPIFY_CLIENT_ID, SHOPIFY_CLIENT_SECRET, SHOPIFY_TOKEN_SECRET (rotate to invalidate), optional SHOPIFY_SCOPES.
    • Users should reconnect via OAuth; removed the storeDomain field, apiVersion field, and the token auth block from app.json.
    • Local dev fallback remains: SHOPIFY_STORE_DOMAIN + SHOPIFY_ACCESS_TOKEN or a raw Authorization: Bearer shpat_….

Written for commit 735f84d. Summary will update on new commits.

Review in cubic

guitavano and others added 3 commits July 28, 2026 12:49
Replaces the manual custom-app token flow with a one-click OAuth grant.
Shopify's authorize endpoint is per-store but the runtime's
authorizationUrl(callbackUrl) hook doesn't know the store, so (like the
WhatsApp MCP) the hook points at our own /oauth/custom page which asks for
the shop domain, then drives the real Shopify grant and hands the mesh a
sealed credential.

- server/lib/token.ts: stateless crypto — HMAC-signed state + AES-256-GCM
  sealed { shop, token } credential (so the Admin token never travels
  readable through browser redirects) + Shopify callback HMAC verification.
- server/lib/oauth.ts: oauth hook (authorizationUrl/exchangeCode) and the
  /oauth/custom + /oauth/shopify/callback route handlers. Offline token, no
  refresh. Requested scopes trimmed to what a standard store can grant
  (Plus/Payments-gated scopes excluded) and overridable via SHOPIFY_SCOPES.
- server/lib/client.ts: resolveCredentials opens the sealed credential
  (both shop + token come from it); legacy raw-token + env-var paths kept.
- types/env.ts + app.json: drop the storeDomain config field and the token
  auth block; refresh descriptions.
- deploy.ts / deploy.yml / SECRETS.md: wire SHOPIFY_CLIENT_ID,
  SHOPIFY_CLIENT_SECRET, SHOPIFY_TOKEN_SECRET, SHOPIFY_SCOPES.
- README: rewrite setup/auth notes for OAuth.

Tests: 51 pass (token sign/seal, HMAC, route handlers, credential resolution).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The MCP now has no connection config: auth is OAuth and the store comes from
the token. The Admin API version still defaults to DEFAULT_API_VERSION and can
be overridden per deploy via the SHOPIFY_API_VERSION env var.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@guitavano
guitavano merged commit e3a4235 into main Jul 28, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant