v0.3.0 AI Drafted

Shopify Plus Hardening Guide

Productivity Last updated: 2026-09-25

E-commerce platform hardening for Shopify Plus including SAML SSO, staff permissions, and store security

View:

Overview

Shopify is a leading e-commerce platform powering millions of businesses worldwide. As a platform handling customer data, payment information, and business transactions, Shopify security configurations directly impact data protection and PCI compliance.

Intended Audience

  • Security engineers managing e-commerce platforms
  • IT administrators configuring Shopify Plus
  • E-commerce managers securing stores
  • GRC professionals assessing retail security

How to Use This Guide

  • L1 (Crawl): Essential controls for all organizations
  • L2 (Walk): Enhanced controls for security-sensitive environments
  • L3 (Run): Strictest controls for regulated industries

Scope

This guide covers Shopify Plus security including SAML SSO, organization management, staff permissions, and store security.

Shopify’s automation surfaces are the Admin GraphQL API and the Shopify CLI, whose shopify store execute runs the same Admin API operations. Shopify publishes no Terraform provider. Most settings in this guide have no write interface on either surface, so every control states its automation verdict, and the Code Packs here are read-only audits.


Table of Contents

  1. Authentication & SSO
  2. Access Controls
  3. Store Security
  4. Compliance Quick Reference

1. Authentication & SSO

1.1 Configure SAML Single Sign-On

Profile Level: L1 (Crawl)

Framework Control
CIS Controls 6.3, 12.5
NIST 800-53 IA-2, IA-8

Description

Configure SAML SSO for Shopify Plus organization users.

Rationale

Why This Matters:

  • Centralizes Shopify Plus authentication in your corporate IdP, enforcing MFA and conditional access on every staff login
  • Local Shopify password logins bypass IdP controls and are prime targets for credential stuffing and phishing
  • Centralized deprovisioning through the IdP removes a departing employee’s access instantly, eliminating orphaned accounts with standing reach into orders and customer data
  • Shopify admin holds customer PII, order history, and payout settings, so a single compromised staff login can expose all of it

Attack Prevented: Credential theft, phishing, password reuse, orphaned-account access

Prerequisites

  • Shopify Plus plan
  • Organization owner access
  • SAML 2.0 compatible IdP
  • A verified domain. Shopify requires domain verification before SAML enforcement can be applied, and domain verification also gates the other organization-level user security features (Managing additional user security features)

ClickOps Implementation

Step 1: Access Organization Settings

  1. Navigate to: Shopify admin → Settings → Users
  2. Access organization settings
  3. Find the Security section

Step 2: Configure SAML

  1. Enable SAML authentication
  2. Configure IdP settings:
    • SSO URL
    • Entity ID
    • Certificate
  3. Download Shopify metadata for IdP

Step 3: Test and Choose an Enforcement Level

  1. Test SSO authentication before enforcing
  2. Set the SAML enforcement level. Shopify documents three discrete levels (SAML authentication):

    Level Who must use SAML
    Required All users with a matching email domain — explicitly including store owners and users outside the organization
    Specific users Only the users you individually select
    Off No one; SAML is available but never enforced
  3. Prefer Required — the middle level leaves password-login paths open for everyone you did not enumerate
  4. Configure and document admin fallback before enforcing, since Required captures store owners too

Time to Complete: ~1-2 hours

Code Implementation

Code Pack: API Script
hth-shopify-1.01-audit-saml-account-types.sh View source on GitHub ↗
# Which active staff accounts sign in through SAML, and which still use
# Shopify credentials outside the documented break-glass exceptions.
audit() {
  fetch_staff
  local exceptions saml_active outside rc=0
  exceptions=$(jq -nc --arg s "${SHOPIFY_SAML_EXCEPTION_IDS:-}" \
    '$s | split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0))')

  echo "Shopify 1.1 — staff sign-in type (store ${SHOPIFY_STORE}, API ${SHOPIFY_API_VERSION})"
  printf '%s' "${STAFF}" | jq -r 'group_by(.accountType // "UNSET")[]
    | "    \(.[0].accountType // "UNSET"): \(length) total, \([.[] | select(.active)] | length) active"'

  saml_active=$(printf '%s' "${STAFF}" | jq '[.[] | select(.active and .accountType == "SAML")] | length')
  outside=$(printf '%s' "${STAFF}" | jq -c --argjson ex "${exceptions}" '[.[]
    | select(.active and .accountType == "REGULAR")
    | select(.id as $i | ($ex | index($i)) == null)
    | .id + (if .isShopOwner then " (store owner)" else "" end)]')

  if [ "${saml_active}" -eq 0 ]; then
    echo "FINDING: no active staff account is SAML-typed — staff are not signing in through your IdP."
    rc=1
  fi
  if [ "$(printf '%s' "${outside}" | jq 'length')" -gt 0 ]; then
    echo "FINDING: active accounts still signing in with Shopify credentials (not in SHOPIFY_SAML_EXCEPTION_IDS):"
    printf '%s' "${outside}" | jq -r '.[] | "    - \(.)"'
    echo "  Under the Required level these should be SAML; confirm each in Settings -> Users (TRAP 2)."
    rc=1
  fi
  [ "$(printf '%s' "${exceptions}" | jq 'length')" -eq 0 ] ||
    echo "  documented exceptions: $(printf '%s' "${exceptions}" | jq 'length')"
  [ "${rc}" -ne 0 ] || echo "NO FINDING: ${saml_active} active SAML account(s); no unexcepted Shopify-credential account."
  return "${rc}"
}

Automation: Configuring and enforcing SAML is ClickOps only. Shopify exposes no write interface for this setting (SAML authentication, 2026-09-24). None of the 470 mutations in Admin GraphQL API version 2026-07 configures SAML, and the Shopify CLI runs only those same Admin API operations. The pack above is the read side. It lists each active staff account’s accountType and names the active accounts that still sign in with Shopify credentials instead of SAML. Put your documented admin fallback account in its exception list. The pack needs the read_users scope, which Shopify limits to Plus and Advanced stores and enables for an app only through Shopify Support.


1.2 Enforce Two-Factor Authentication

Profile Level: L1 (Crawl)

Framework Control
CIS Controls 6.5
NIST 800-53 IA-2(1)

Description

Require 2FA for all Shopify staff accounts.

Rationale

Why This Matters:

  • A second authentication factor blocks login even when a staff password is stolen, leaked, or reused across sites
  • E-commerce admin accounts are high-value targets for takeover that redirects payouts, alters store content, or exfiltrates customer data
  • Phishing-resistant factors such as security keys and authenticator apps defeat the credential-replay attacks aimed at retail platforms
  • Requiring 2FA organization-wide closes the gap left by individual staff who would otherwise opt out

Attack Prevented: Credential stuffing, password reuse, phishing, account takeover

ClickOps Implementation

Step 1: Enable 2FA Requirement

  1. Navigate to: Settings → Users → Security
  2. Enable the two-step authentication requirement
  3. All staff must configure two-step authentication

Step 2: Prefer Phishing-Resistant Factors

  1. Have staff register security keys as their two-step authentication method — hardware keys defeat the credential-replay phishing aimed at retail admin accounts
  2. Adopt passkeys, which Shopify documents as “a more secure replacement for passwords” (Account security best practices)
  3. Treat SMS as the fallback of last resort

Step 3: Store Recovery Codes Safely

  1. Have every staff member generate and store their recovery codes in the organization’s password manager, not in email or a local file
  2. Lost recovery codes plus a lost device is an account-recovery incident; unprotected recovery codes are a second credential an attacker can steal

Step 4: Configure via IdP

  1. Enable MFA in the identity provider for SSO-brokered logins
  2. Use phishing-resistant methods for admins

Automation: ClickOps only — Shopify exposes no write interface for this setting (Managing additional user security features, 2026-09-24). The Admin GraphQL StaffMember object has no two-step or MFA field, and none of the 470 mutations in API version 2026-07 sets the requirement. The Shopify CLI adds nothing, because it runs those same Admin API operations. Confirm the requirement and each user’s enrollment in the console.


1.3 Configure Login Services

Profile Level: L2 (Walk)

Framework Control
CIS Controls 6.3
NIST 800-53 IA-2

Description

Control allowed login methods.

Rationale

Why This Matters:

  • Restricting login to SSO-only removes weaker fallback paths that attackers exploit to bypass your IdP
  • Disabling unused authentication options shrinks the attack surface and reduces the number of credential sets to defend
  • Forcing all access through a single monitored channel ensures every login is subject to MFA and conditional-access policy
  • Legacy or social login providers may not enforce your organization’s MFA and session controls

Attack Prevented: Authentication bypass, MFA downgrade, weak-login-path access

ClickOps Implementation

Step 1: Review Login Options

  1. Configure allowed login services
  2. Restrict to SSO only if possible
  3. Disable unnecessary auth methods

Automation: ClickOps only — Shopify exposes no write interface for this setting (GraphQL Admin API, 2026-09-24). No query or mutation in API version 2026-07 reads or sets which login methods staff may use, and the Shopify CLI runs only those same Admin API operations.


1.4 Automate User Management with SCIM

Profile Level: L2 (Walk)

Framework Control
CIS Controls 5.1, 5.3
NIST 800-53 AC-2, AC-2(1)

Description

Enable SCIM user management on the Shopify Plus organization and generate the SCIM API token so your identity provider creates, updates, and deactivates Shopify users automatically instead of an admin doing it by hand.

Rationale

Why This Matters:

  • Deprovisioning that fires from the IdP removes a departing employee’s Shopify access at the same instant as everything else, closing the offboarding gap that leaves standing access to orders, customer PII, and payout settings
  • Manual user administration across a multi-store organization reliably drifts — SCIM makes the IdP the authoritative record of who holds Shopify access
  • The SCIM API token is itself a high-value credential: it can manipulate the full user roster, so it must be stored in a secrets manager and rotated on schedule

Attack Prevented: Orphaned-account access, offboarding gaps, unauthorized account creation, access drift across stores

Prerequisites

  • Shopify Plus plan
  • Organization owner access
  • A verified domain
  • An IdP that supports SCIM provisioning

ClickOps Implementation

Step 1: Enable SCIM

  1. Navigate to: Shopify admin → Settings → Users → Security
  2. Enable SCIM user management and generate the SCIM API token (Managing additional user security features)
  3. Copy the token once — treat it as a secret with organization-wide user-management power

Step 2: Connect the IdP

  1. Configure the Shopify SCIM connector in your identity provider using the generated token
  2. Map IdP attributes to Shopify user attributes
  3. Enable deactivation-on-deprovision

Step 3: Protect and Rotate the Token

  1. Store the token in your secrets manager, never in a shared doc or ticket
  2. Rotate it on a defined schedule and immediately on any suspected exposure
  3. Verify with a test user that IdP deactivation removes Shopify access

Automation: ClickOps only — Shopify exposes no write interface for this setting (Managing additional user security features, 2026-09-24). You generate the SCIM token in the console, and your identity provider consumes it. Shopify documents no SCIM endpoint for direct use, and no Admin GraphQL mutation in API version 2026-07 enables SCIM or issues the token. Once SCIM is connected, provisioning itself runs from the IdP.


2. Access Controls

2.1 Configure Staff Permissions

Profile Level: L1 (Crawl)

Framework Control
CIS Controls 5.4
NIST 800-53 AC-6

Description

Implement least privilege for staff accounts using Shopify roles — predefined roles plus custom roles organized into role categories — and retire any permission sets still carrying the “Legacy access” badge.

Rationale

Why This Matters:

  • Least-privilege roles ensure a compromised staff account can only reach the data and actions its role strictly needs
  • Separating duties across orders, products, customers, and reports limits the blast radius of any single account takeover or insider misuse
  • Over-broad permissions let routine staff export customer PII or change payout settings far beyond their job function
  • Accounts still on auto-converted legacy permissions carry whatever they happened to hold at migration, which is rarely what a deliberate role design would grant
  • Regular access reviews catch privilege creep and remove leftover access after role changes

Attack Prevented: Privilege escalation, insider data theft, lateral movement, excessive data exposure

Deprecated mechanism (as of May 1, 2025): Shopify retired the standalone staff custom permission groups model. Legacy permissions were automatically converted to roles, and converted assignments surface with a Legacy access badge in Settings → Users. Guidance that tells you to “create a custom permission group” describes a model that no longer exists — build custom roles instead, and treat every remaining “Legacy access” badge as an unreviewed grant. Source: Migrate to roles

ClickOps Implementation

Step 1: Review Roles

  1. Navigate to: Settings → Users → Roles
  2. Review the predefined roles Shopify ships and the custom roles your organization has created
  3. Review role categories to understand how roles are grouped and applied

Step 2: Clear Legacy Access

  1. In Settings → Users, filter for accounts carrying the Legacy access badge
  2. For each, decide the correct predefined or custom role and reassign
  3. A remaining Legacy access badge means nobody has consciously approved that user’s permission set since the migration

Step 3: Assign Minimum Access

  1. Assign the least-privileged role that lets the person do their job
  2. Separate by function:
    • Store management
    • Orders/fulfillment
    • Products
    • Customers
    • Reports
  3. Run regular access reviews

Step 4: Cover Newly Shipped Permissions

  1. On 2026-07-07 Shopify shipped four new staff permissions covering payments, payouts, disputes, and tax documents (Shopify changelog)
  2. These are exactly the money-movement and financial-record surfaces least-privilege reviews exist to constrain — audit who holds them rather than inheriting a default
  3. Add a changelog check to your periodic access review so newly introduced permissions do not accumulate unreviewed

Automation: ClickOps only — Shopify exposes no write interface for this setting (Migrate to roles, 2026-09-24). The Admin GraphQL API in version 2026-07 has no staff role object and no mutation that assigns a staff role. The companyContact…Role mutations govern B2B customer roles, not staff. The only staff permissions field, StaffMemberPrivateData.permissions, is deprecated. Review roles and Legacy access badges in the console.


2.2 Configure Store Access

Profile Level: L2 (Walk)

Framework Control
CIS Controls 5.4
NIST 800-53 AC-6

Description

Control access to individual stores.

Rationale

Why This Matters:

  • Scoping staff to only the stores they operate prevents one compromised account from reaching every store in the organization
  • Separating production from development stores keeps test access from exposing live customer and order data
  • Auditing cross-store access surfaces accounts that have quietly accumulated reach beyond their responsibilities
  • In multi-store organizations, unpartitioned access multiplies the blast radius of any single breach

Attack Prevented: Lateral movement across stores, unauthorized data access, blast-radius expansion

ClickOps Implementation

Step 1: Configure Store Permissions

  1. Limit staff to required stores only
  2. Separate production and development
  3. Audit cross-store access

Automation: ClickOps only — Shopify exposes no write interface for this setting (GraphQL Admin API, 2026-09-24). The Admin API works on one store at a time and cannot see which other stores in the organization a user can reach. Its only staff-assignment mutations, companyLocationAssignStaffMembers and companyLocationRemoveStaffMembers, scope B2B company locations, not store access.


2.3 Limit Admin Access

Profile Level: L1 (Crawl)

Framework Control
CIS Controls 5.4
NIST 800-53 AC-6(1)

Description

Minimize and protect organization owner accounts.

Rationale

Why This Matters:

  • Organization owners hold the highest privileges, so fewer owners means fewer high-value accounts an attacker can target
  • Owners can change billing, add or remove staff, and alter security settings, meaning one compromised owner controls the entire organization
  • Requiring 2FA and monitoring owner activity makes takeover of these accounts substantially harder and faster to detect
  • Documenting and pruning owner access prevents standing super-admin privileges from outliving their need

Attack Prevented: Privilege escalation, full-organization takeover, unauthorized security-setting changes

ClickOps Implementation

Step 1: Inventory Owners

  1. Review organization owners
  2. Document admin access
  3. Identify unnecessary privileges

Step 2: Apply Restrictions

  1. Limit owners to 2-3 users
  2. Require 2FA for owners
  3. Monitor owner activity

Code Implementation

Code Pack: API Script
hth-shopify-2.03-audit-owner-and-staff-access.sh View source on GitHub ↗
# Owner consistency, pending ownership transfer, and standing partner access.
audit() {
  gql 'query HthShopOwner { shop { accountOwner { id } } }' '{}'
  local owner_id
  owner_id=$(printf '%s' "${GQL_BODY}" | jq -r '.data.shop.accountOwner.id // ""')
  [ -n "${owner_id}" ] || { echo "PRECONDITION: shop.accountOwner came back empty" >&2; exit 2; }

  fetch_staff

  echo "Shopify 2.3 — owner and staff-access inventory (store ${SHOPIFY_STORE}, API ${SHOPIFY_API_VERSION})"
  echo "  staff accounts: $(printf '%s' "${STAFF}" | jq 'length') (active $(printf '%s' "${STAFF}" | jq '[.[] | select(.active)] | length'))"
  printf '%s' "${STAFF}" | jq -r 'group_by(.accountType // "UNSET")[]
    | "    \(.[0].accountType // "UNSET"): \(length) total, \([.[] | select(.active)] | length) active"'

  local rc=0 flagged owners
  owners=$(printf '%s' "${STAFF}" | jq -c '[.[] | select(.isShopOwner) | .id]')
  if [ "$(printf '%s' "${owners}" | jq --arg o "${owner_id}" '. == [$o]')" != "true" ]; then
    echo "FINDING: store ownership is inconsistent — shop.accountOwner ${owner_id} vs isShopOwner ${owners}."
    rc=1
  fi

  # INVITED_STORE_OWNER: "The user has not yet accepted the invitation to become
  # the store owner." Whoever accepts takes the store's highest privilege.
  flagged=$(printf '%s' "${STAFF}" | jq -c '[.[] | select(.accountType == "INVITED_STORE_OWNER") | .id]')
  if [ "$(printf '%s' "${flagged}" | jq 'length')" -gt 0 ]; then
    echo "FINDING: pending store-owner invitation(s): ${flagged}"
    echo "  Confirm the transfer was intended, or revoke it in Settings -> Users."
    rc=1
  fi

  # Partner collaborators and their team members hold admin access from outside
  # the organization; REQUESTED is a collaborator request awaiting a decision.
  flagged=$(printf '%s' "${STAFF}" | jq -c '[.[] | select(.active)
    | select(.accountType == "COLLABORATOR" or .accountType == "COLLABORATOR_TEAM_MEMBER") | .id]')
  [ "$(printf '%s' "${flagged}" | jq 'length')" -eq 0 ] ||
    echo "REVIEW: active partner collaborator accounts — confirm each is still needed: ${flagged}"
  flagged=$(printf '%s' "${STAFF}" | jq -c '[.[] | select(.accountType == "REQUESTED") | .id]')
  [ "$(printf '%s' "${flagged}" | jq 'length')" -eq 0 ] ||
    echo "REVIEW: collaborator access requests awaiting a decision: ${flagged}"

  echo "  NOT COVERED (console only): organization owners, per-staff permissions, owner 2FA (TRAPS 2-3)."
  [ "${rc}" -ne 0 ] || echo "NO FINDING: one consistent store owner and no pending ownership transfer."
  return "${rc}"
}
Code Pack: CLI Script
hth-shopify-2.03-audit-owner-and-staff-access.sh View source on GitHub ↗
# The same inventory as the api/ pack, read through `shopify store execute`
# (queries only; --allow-mutations is never passed).
audit() {
  run_query 'query HthShopOwner { shop { accountOwner { id } } }' '{}'
  local owner_id
  owner_id=$(printf '%s' "${RESULT}" | jq -r '.shop.accountOwner.id // ""')
  [ -n "${owner_id}" ] || { echo "PRECONDITION: shop.accountOwner came back empty" >&2; exit 2; }

  fetch_staff

  echo "Shopify 2.3 (CLI) — owner and staff-access inventory (store ${SHOPIFY_STORE}, API ${SHOPIFY_API_VERSION})"
  printf '%s' "${STAFF}" | jq -r 'group_by(.accountType // "UNSET")[]
    | "    \(.[0].accountType // "UNSET"): \(length) total, \([.[] | select(.active)] | length) active"'

  local rc=0 flagged owners
  owners=$(printf '%s' "${STAFF}" | jq -c '[.[] | select(.isShopOwner) | .id]')
  if [ "$(printf '%s' "${owners}" | jq --arg o "${owner_id}" '. == [$o]')" != "true" ]; then
    echo "FINDING: store ownership is inconsistent — shop.accountOwner ${owner_id} vs isShopOwner ${owners}."
    rc=1
  fi
  flagged=$(printf '%s' "${STAFF}" | jq -c '[.[] | select(.accountType == "INVITED_STORE_OWNER") | .id]')
  if [ "$(printf '%s' "${flagged}" | jq 'length')" -gt 0 ]; then
    echo "FINDING: pending store-owner invitation(s): ${flagged}"
    rc=1
  fi
  flagged=$(printf '%s' "${STAFF}" | jq -c '[.[] | select(.active)
    | select(.accountType == "COLLABORATOR" or .accountType == "COLLABORATOR_TEAM_MEMBER") | .id]')
  [ "$(printf '%s' "${flagged}" | jq 'length')" -eq 0 ] ||
    echo "REVIEW: active partner collaborator accounts — confirm each is still needed: ${flagged}"
  flagged=$(printf '%s' "${STAFF}" | jq -c '[.[] | select(.accountType == "REQUESTED") | .id]')
  [ "$(printf '%s' "${flagged}" | jq 'length')" -eq 0 ] ||
    echo "REVIEW: collaborator access requests awaiting a decision: ${flagged}"

  echo "  NOT COVERED (console only): organization owners, per-staff permissions, owner 2FA (TRAP 3)."
  [ "${rc}" -ne 0 ] || echo "NO FINDING: one consistent store owner and no pending ownership transfer."
  return "${rc}"
}

Automation: Both packs above are read-only inventories of the store owner (shop.accountOwner) and every staff account’s type, one through the Admin API and one through the Shopify CLI. They flag an inconsistent owner and a pending store-owner invitation, and list partner collaborator accounts for review. Limiting organization owners and requiring owner 2FA are ClickOps only. Shopify exposes no write interface for this setting (StaffMember, 2026-09-24). Organization owners are invisible to the store-scoped Admin API, per-staff permissions are deprecated there, and no mutation changes a staff account. Both packs need the read_users scope, which Shopify limits to Plus and Advanced stores and enables for an app only through Shopify Support.


2.4 Manage Access with User Groups

Profile Level: L2 (Walk)

Framework Control
CIS Controls 5.4, 6.8
NIST 800-53 AC-2, AC-6

Description

On Shopify Plus, use user groups to grant roles by membership rather than assigning roles to each staff account individually, so joiners and movers inherit exactly the access their team is supposed to have.

Rationale

Why This Matters:

  • Group-inherited roles make access reviews tractable: you review a handful of groups instead of auditing every account’s bespoke role stack
  • Onboarding by group membership stops the common failure of copying an existing user’s permissions and silently propagating over-broad access
  • Group membership changes are a single, auditable event, which makes role changes easier to reconstruct during an investigation
  • Access granted individually outside groups tends to be invisible in reviews and outlives the reason it was granted

Attack Prevented: Permission creep, over-broad access inheritance, unreviewed standing privileges, incomplete role-change cleanup

Prerequisites

  • Shopify Plus plan
  • Organization owner access

ClickOps Implementation

Step 1: Design Groups Around Job Functions

  1. Create user groups that mirror actual job functions, not org-chart convenience
  2. Attach the least-privileged role each function needs to its group (User groups)

Step 2: Understand Removal Semantics

  1. Removing a user from a group strips only the permissions that group granted — any role assigned to the user directly survives the removal
  2. Because of this, directly-assigned roles are the ones that quietly outlive a transfer or departure; audit them separately from group grants
  3. When a user changes teams, remove the old group AND re-check their direct assignments

Step 3: Review

  1. Review group membership and group-attached roles on the same cadence as your staff access review
  2. Prefer moving direct assignments into groups so future reviews cover them automatically

Automation: ClickOps only — Shopify exposes no write interface for this setting (User groups, 2026-09-24). No Admin GraphQL object or mutation in API version 2026-07 manages staff user groups, and the Shopify CLI runs only those same Admin API operations.


3. Store Security

3.1 Configure API Access

Profile Level: L2 (Walk)

Framework Control
CIS Controls 3.11
NIST 800-53 SC-12

Description

Secure API apps and access tokens.

Rationale

Why This Matters:

  • API apps and access tokens are non-interactive credentials that often bypass MFA, making over-scoped tokens a direct path to bulk data exfiltration
  • Removing unnecessary apps shrinks the third-party supply-chain surface, since a breached vendor app can read orders and customer PII
  • Minimum-scope tokens limit what a leaked credential can do, and regular rotation shortens the window a stolen token stays valid
  • Shopify’s history includes third-party app vendors exposing thousands of stores’ data through over-privileged integrations

Attack Prevented: Token theft, over-scoped API abuse, third-party app compromise, bulk data exfiltration

ClickOps Implementation

Step 1: Review Apps

  1. Navigate to: Settings → Apps and sales channels
  2. Review installed apps
  3. Remove unnecessary apps

Step 2: Secure API Credentials

  1. Use minimum required scopes
  2. Protect API credentials
  3. Rotate credentials regularly

Step 3: Meet the Protected Customer Data Requirements

  1. Any app accessing protected customer data must satisfy Shopify’s documented requirements (Protected customer data). Apply the same bar when vetting third-party apps and when building your own:

    Level Requirements
    Level 1 — protected customer data Data minimization, purpose limitation, retention limits, encryption at rest and in transit
    Level 2 — protected customer fields (name, email, phone, address) Everything in Level 1, plus encrypted backups, separation of test and production environments, data loss prevention, staff access restrictions, access logging, and a documented incident response policy
  2. Before installing an app that requests protected customer fields, ask the vendor which level it operates at and how it satisfies the Level 2 items — most breaches of Shopify merchant data have come through third-party apps, not Shopify itself
  3. For in-house apps, treat this list as the minimum control set, not as guidance

Code Implementation

Code Pack: API Script
hth-shopify-3.01-audit-app-token-scopes.sh View source on GitHub ↗
# Compare the scopes this app's token actually holds with the scopes it is
# meant to hold. Any extra handle is a finding.
audit() {
  gql 'query HthAppScopes { currentAppInstallation { accessScopes { handle } } }' '{}'
  printf '%s' "${GQL_BODY}" | jq -e '.data.currentAppInstallation.accessScopes | type == "array"' >/dev/null || {
    echo "PRECONDITION: the token did not resolve to an app installation (TRAP 3)" >&2; exit 2; }

  local granted expected excess missing
  granted=$(printf '%s' "${GQL_BODY}" | jq -c '[.data.currentAppInstallation.accessScopes[].handle] | unique')
  expected=$(jq -nc --arg s "${SHOPIFY_EXPECTED_SCOPES}" \
    '$s | split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0)) | unique')
  [ "$(printf '%s' "${expected}" | jq 'length')" -gt 0 ] || {
    echo "PRECONDITION: SHOPIFY_EXPECTED_SCOPES parsed to an empty list" >&2; exit 2; }
  excess=$(jq -nc --argjson g "${granted}" --argjson e "${expected}" '$g - $e')
  missing=$(jq -nc --argjson g "${granted}" --argjson e "${expected}" '$e - $g')

  echo "Shopify 3.1 — granted scopes for this app's token (store ${SHOPIFY_STORE}, API ${SHOPIFY_API_VERSION})"
  echo "  granted:  $(printf '%s' "${granted}" | jq -r 'length') — $(printf '%s' "${granted}" | jq -r 'join(", ")')"
  echo "  expected: $(printf '%s' "${expected}" | jq -r 'length') — $(printf '%s' "${expected}" | jq -r 'join(", ")')"
  [ "$(printf '%s' "${missing}" | jq 'length')" -eq 0 ] ||
    echo "  note: expected but not granted (not a security finding): $(printf '%s' "${missing}" | jq -r 'join(", ")')"

  if [ "$(printf '%s' "${excess}" | jq 'length')" -gt 0 ]; then
    echo "FINDING: this token holds scopes beyond the expected list: $(printf '%s' "${excess}" | jq -r 'join(", ")')"
    echo "  Remove them from the app configuration and redeploy, or revoke optional scopes with appRevokeAccessScopes."
    return 1
  fi
  echo "NO FINDING: every granted scope is on the expected list."
}

Automation: Reviewing and removing third-party apps is ClickOps only. Shopify exposes no write interface for this setting (appUninstall, 2026-09-24). appUninstall and appRevokeAccessScopes act only on the app making the call. Shopify documents the appInstallations query as listing installations “across multiple stores where your app is installed”, not as an inventory of the other apps on your store. For apps you build, the scope surface is real. Declare the minimum in the [access_scopes] section of the app configuration and release it with shopify app deploy (App configuration). The pack above then reads the scopes a token actually holds and flags any beyond your expected list. It needs no extra scope. Shopify also documents mutations for two kinds of token an app issues itself. delegateAccessTokenCreate mints a token carrying a subset of the calling app’s scopes, with an optional expiresIn, and delegateAccessTokenDestroy revokes it (delegateAccessTokenCreate, 2026-09-24). storefrontAccessTokenCreate and storefrontAccessTokenDelete do the same for Storefront API tokens (storefrontAccessTokenDelete, 2026-09-24). No pack wraps them here, because each call mints or revokes a live credential.


3.2 Configure Checkout Security

Profile Level: L1 (Crawl)

Framework Control
CIS Controls 3.10
NIST 800-53 SC-8

Description

Configure secure checkout settings.

Rationale

Why This Matters:

  • HTTPS on checkout protects payment and personal data in transit from interception and man-in-the-middle attacks
  • Fraud analysis flags high-risk orders before fulfillment, reducing chargebacks and abuse from stolen payment cards
  • reCAPTCHA blocks automated bots from carding attacks, credential stuffing, and fake-account creation at checkout
  • The checkout flow is where customer payment data is most exposed, making it a primary target for skimming and fraud

Attack Prevented: Man-in-the-middle interception, payment fraud, carding, bot abuse

ClickOps Implementation

Step 1: Review Checkout Settings

  1. Ensure HTTPS enabled (default)
  2. Configure fraud analysis
  3. Enable reCAPTCHA

Automation: ClickOps only — Shopify exposes no write interface for this setting (GraphQL Admin API, 2026-09-24). The only checkout configuration mutation in API version 2026-07, checkoutAndAccountsConfigurationUpdate, carries branding settings only. The Shop object has no fraud-analysis or CAPTCHA setting.


4. Compliance Quick Reference

SOC 2 Trust Services Criteria Mapping

Control ID Shopify Control Guide Section
CC6.1 SSO/2FA 1.1
CC6.2 Staff permissions 2.1
CC6.7 API security 3.1

PCI DSS v4.0 Mapping

Requirement Shopify Control Guide Section
7 Staff permissions 2.1
8 Authentication 1.1

Appendix A: References

Official Shopify Documentation:

Link note (2026-08-08): the SAML documentation moved from /manual/shopify-plus/saml to /manual/organization-settings/users/security/saml; the old path now renders a generic page rather than the SAML article.

Link note (2026-09-24): it moved again. Both /manual/organization-settings/users/security and its /saml child now answer 307 and redirect to /manual/your-account/users/security/advanced-security-features and .../advanced-security-features/saml. The links above point at the new paths.

API & Developer Tools:

Compliance Frameworks:

  • PCI DSS Level 1 (Service Provider), SOC 2 Type II, SOC 3 — merchants retrieve current attestations through the admin per Viewing Shopify’s Compliance Reports. Compliance reports describe Shopify’s own posture; they are not merchant configuration guidance.

Security Incidents:

  • (2020) Two rogue support team members accessed data from approximately 200 merchants.
  • (2024) Third-party app vendor (Saara) exposed 25 GB of data from 1,800+ Shopify stores via a misconfigured MongoDB database. Separately, a threat actor claimed to have 179,873 rows of user data.
  • (2025-01) Critical vulnerability in the Consentik Shopify app exposed 4,180+ stores to code injection and account takeover.

Changelog

Date Version Maturity Changes Author
2026-09-25 0.3.0 ai-drafted Fix pass from a validate-hth-guide run (Phase 5). Code Packs landed for the first time, all read-only: api/ 1.1 (staff sign-in type: SAML vs Shopify credentials), api/ 2.3 and cli/ 2.3 (store owner and staff-access inventory, the CLI one via shopify store execute), and api/ 3.1 (a token’s granted scopes compared with an expected list). Each was transcribed from the Admin GraphQL 2026-07 reference and the Shopify CLI command reference. None was executed against a store. Every control now carries an automation verdict. A census of all 470 Admin GraphQL 2026-07 mutations, the Shopify CLI, and the Terraform Registry (no Shopify provider) found no write interface for SAML, two-step authentication, login methods, SCIM, staff roles, store access, user groups, third-party app removal, or checkout fraud/CAPTCHA settings. 1.2, 1.3, 1.4, 2.1, 2.2, 2.4 and 3.2 state that as **Automation:** ClickOps only, and 1.1, 2.3 and 3.1 pair it with their read packs. 3.1 also names the delegate and Storefront access-token mutations, which the first census missed. The three api/ packs treat an errors member of any shape as a failure and exit 2. Citations: the SAML and user-security help pages moved a second time, and 1.1, 1.4 and Appendix A now point at /manual/your-account/users/security/advanced-security-features. Appendix A drops “App Developer Tools & SDKs”, which now redirects to the Admin API Reference already listed. No live exercise: Shopify sign-in was not available to the run, so no console path was walked, no pack was run, and the maturity set is unchanged. Claude Code (Opus 5.5)
2026-08-08 0.2.0 ai-drafted Currency pass: rewrite 2.1 around roles after the May 1 2025 retirement of custom permission groups (Legacy access badge) and add the 2026-07-07 payments/payouts/disputes/tax-document permissions; repoint SAML docs to the current canonical URL; document the three SAML enforcement levels and the verified-domain prerequisite; add 1.4 SCIM user management and 2.4 user groups; add passkeys, security keys, and recovery-code handling to 1.2; add the protected customer data Level 1/Level 2 requirements to 3.1; remove the Trust Center and compliance-reports marketing links from Appendix A. Tier 3/4 sources not surveyed this pass. Claude Code (Opus 5)
2025-02-05 0.1.0 ai-drafted Initial guide with SSO and permissions Claude Code (Opus 4.5)

Contributing

Found an issue or want to improve this guide?