Security & Compliance
Multi-layered authentication per surface, GDPR-compliant data handling, webhook idempotency, automated tax exemption, and EU reverse charge enforcement.
Authentication Per Surface
Each surface uses the authentication method appropriate to its execution context. Cart uses App Proxy HMAC, UI Extensions use Session Token JWT, and the Admin dashboard relies on standard Shopify OAuth.
| Surface | Auth Method | Details |
|---|---|---|
| Cart (App Proxy) | HMAC | Shopify signs request with app secret, timing-safe comparison |
| Checkout | JWT | sessionToken.get() in extension |
| Thank You | JWT | sessionToken.get() in extension |
| Customer Account | JWT | sessionToken.get() in extension |
| Admin | OAuth | Standard Shopify app auth flow |
| External API | Bearer | API key from Pro plan (post-MVP) |
App Proxy HMAC Verification
Shopify signs every App Proxy request with the application's API secret. The backend verifies authenticity through HMAC-SHA256 with timing-safe comparison to prevent signature forgery.
- All query parameters collected from request
- Parameters sorted alphabetically by key
- HMAC-SHA256 digest computed against sorted params
- Hex-encoded digest appended to forwarded request
crypto.timingSafeEqual to prevent timing attacks that could leak valid signatures byte by byte.- Constant-time buffer comparison
- Prevents statistical timing analysis
- Returns 401 on mismatch, no error details
- IP extracted from
X-Forwarded-Forheader
// Sorted params → HMAC-SHA256 → hex comparison const sortedParams = Object.keys(query) .filter(key => key !== 'signature') .sort() .map(key => `${key}=${query[key]}`) .join(''); const digest = crypto .createHmac('sha256', API_SECRET) .update(sortedParams) .digest('hex'); // Timing-safe: prevents timing attacks const isValid = crypto.timingSafeEqual( Buffer.from(digest), Buffer.from(signature) );
Session Token Handling
UI Extensions authenticate using JWT tokens obtained from Shopify's SDK. A wrapper pattern handles automatic token refresh with double retry for seamless error recovery.
SESSION TOKEN LIFECYCLE +-------------------+ +-------------------+ | UI Extension | | Shopify SDK | | Checkout | | | | Thank You | ──1──> | sessionToken | | Customer Account | <──2── | .get() | +--------+----------+ +-------------------+ | | 3 Authorization: Bearer <JWT> v +-------------------+ +-------------------+ | Backend API | | 401 Response? | | Verify JWT | ──4──> | | | Extract shop | | Yes → Get new token| | Process request | | → Retry request| +-------------------+ | → Still 401? | | Show error | +-------------------+
- Extensions call
sessionToken.get()from Shopify SDK - Returns a signed JWT containing shop domain and session data
- Token sent as
Authorization: Bearerheader - Backend verifies JWT signature and extracts shop context
fetchWithTokenRefreshwrapper used in all extensions- 401 response triggers automatic token refresh
- Request retried with new token
- If still 401 on second attempt, show error to user
Shopify Required Scopes
Minimum required OAuth scopes for reading and writing order and customer data. Each scope is justified by a specific functional requirement.
taxExempt property for automatic VAT exemption on validated B2B customers.read_checkouts and write_checkouts are deprecated by Shopify. Checkout UI Extensions use session token-based authentication and do not require these scopes.Webhook Security
Five mandatory webhooks handle lifecycle events, GDPR compliance, and order synchronization. All webhooks are verified via Shopify's HMAC-SHA256 signature and processed with idempotency guarantees.
webhook_logs table enforces a unique index on (order_id, topic) to detect duplicate deliveries at the database level.23505 (unique_violation). The handler catches this and skips processing.GDPR Compliance
Comprehensive GDPR compliance with clear data classification, purpose-specific retention periods, and automated anonymization. VAT business data is preserved for tax compliance while personal data is immediately redacted.
Data Classification
| Data Type | Classification | GDPR Scope |
|---|---|---|
| VAT numbers | Business data | Outside GDPR |
| Company names | Business data | Outside GDPR |
| Customer email | Personal data (PII) | GDPR Applies |
| Customer ID | Personal data (PII) | GDPR Applies |
| IP address | Personal data (PII) | GDPR Applies |
CUSTOMERS_REDACT Handling
| Data | After Redaction | Retention |
|---|---|---|
customer_email |
NULL (deleted) | Immediate |
ip_address |
NULL (deleted) | Immediate |
customer_id |
REDACTED_{timestamp} | Immediate |
vat_number |
PRESERVED | 7 years (tax compliance) |
company_name |
PRESERVED | 7 years (tax compliance) |
is_valid, validated_at |
PRESERVED | 7 years (tax compliance) |
customer_vat_profiles and vat_validations tables. Returns structured data to Shopify, which the merchant delivers to the requesting customer. This webhook handler is mandatory for Shopify App Store review.Tax Exemption
Automated VAT exemption based on customer login status and plan tier. Logged-in customers on all plans get automatic exemption; guest exemption requires Shopify Plus for customer creation capabilities.
Logged-in Customer All Plans
autoExemptionEnabled in shop settings?exemptionSameCountry=false, skip exemptioncustomer.taxExempt = truevat-exempt to customerGuest Customer Plus Only
vat-auto-createdtaxExempt = true on customerEdge Cases
customerUpdate mutation sets taxExempt: true and adds the vat-exempt tag to the existing customer record.customerCreate mutation creates the customer with taxExempt: true and tags vat-exempt, vat-auto-created.valid. Exemption result: false. Error is logged for admin visibility. Customer sees valid VAT confirmation.Reverse Charge
EU intra-community B2B reverse charge mechanism per Article 196, Council Directive 2006/112/EC. When seller and buyer are in different EU countries with a valid VAT number, the buyer self-assesses VAT.
REVERSE CHARGE EVALUATION LOGIC +-------------------+ | Start Evaluation | +--------+----------+ | v +-------------------+ NO +-------------------+ | RC enabled? | ────────> | Skip (disabled) | +--------+----------+ +-------------------+ | YES v +-------------------+ NULL +-------------------+ | Seller country? | ────────> | Skip + warning | +--------+----------+ +-------------------+ | SET v +-------------------+ NO +-------------------+ | VAT valid? | ────────> | Not applicable | +--------+----------+ +-------------------+ | YES v +-------------------+ YES +-------------------+ | Same country? | ────────> | Not applicable | +--------+----------+ +-------------------+ | NO v +-------------------+ NO +-------------------+ | Both EU members? | ────────> | Not applicable | +--------+----------+ +-------------------+ | YES v +-------------------+ | RC APPLICABLE | Write to order metafields: | reverse_charge | reverse_charge = true | = true | reverse_charge_note = "..." | + tag: reverse-charge Add order tag +-------------------+
GR in ISO 3166-1 but EL in the VIES system. Country codes are normalized before comparison: GR → EL. This normalization applies to both seller and buyer country codes for consistent evaluation.EU Member States (27 + XI)
Integration Points
reverse_charge_note order metafield for invoice display.Admin API Retry
Shopify Admin API calls are protected by a retry strategy that handles rate limits and transient failures with exponential backoff, up to 3 retries maximum.
order-metafield, customer-metafield, tax-exemption, and webhook handlers. Maximum 3 retries per operation. GraphQL rate limit: 40 requests per second.Cart Attribute Security
Cart attributes are client-writable and can be manipulated via JavaScript. A defense-in-depth strategy combines client-side Function validation with server-side webhook re-validation.
DEFENSE IN DEPTH: CART ATTRIBUTE VALIDATION +-------------------+ +-------------------+ | Attacker | | Legitimate User | | JS console | | VAT input widget | | manipulates attrs | | validated value | +--------+----------+ +--------+----------+ | | v v +========================================================+ | LAYER 1: Shopify Function (cart validation) | | Format re-validation as basic protection | | Catches obvious manipulation (invalid format) | +=============================+==========================+ | v +========================================================+ | LAYER 2: ORDERS_CREATE Webhook (server-side) | | Full re-validation against VIES | | Catches everything — authoritative validation | +========================================================+
- Users can open browser console and modify attributes directly
- Browser extensions or injected scripts could alter values
- Attribute values cannot be trusted as-is
- Function: format regex check blocks obviously invalid values at checkout
- Webhook: server-side VIES re-validation on order creation
- Function catches fast; webhook catches everything
SEO / Indexing Protection
Prevents search engines from indexing or displaying VAT numbers entered by customers in search result snippets.
data-nosnippetattribute on the VAT widget container- Prevents Google from using widget content in search snippets
- Widget DOM content excluded from rich results
autocomplete="off"on VAT input field- Prevents browsers from caching VAT numbers in autofill
- Reduces risk of VAT number leakage across sessions
<!-- Prevent search engine indexing of VAT data --> <div class="vat-widget" data-nosnippet> <input type="text" name="vat-number" autocomplete="off" placeholder="e.g. DE123456789" /> </div>