Skip to main content

Authentication

RecoApp implements a multi-layered authentication architecture designed for different access patterns across the Shopify app ecosystem. This page covers all authentication mechanisms used throughout the system.

Overview

RecoApp uses four distinct authentication methods, each serving a specific purpose:

Authentication MethodUse CaseComponents
Shopify OAuth 2.0Merchant installation and app accessrecoapp-shopify
Session Token (JWT)Admin API requests from embedded apprecoapp-api
Pixel API KeysStorefront tracking eventsrecoapp-api, web pixel extension
Internal API KeysServer-to-server communicationrecoapp-shopifyrecoapp-api
Webhook HMACShopify webhook verificationrecoapp-api
info

All authentication mechanisms enforce HTTPS in production. Credentials are encrypted at rest using AES-256-CBC encryption.


1. Shopify OAuth 2.0

Purpose

Authenticates merchants during app installation and establishes authorised access to the Shopify Admin API.

Implementation

Location: recoapp-shopify/app/shopify.server.ts

Configuration:

const shopify = shopifyApp({
apiKey: process.env.SHOPIFY_API_KEY,
apiSecretKey: process.env.SHOPIFY_API_SECRET,
apiVersion: ApiVersion.January25,
scopes: process.env.SCOPES?.split(","),
appUrl: process.env.SHOPIFY_APP_URL,
authPathPrefix: "/auth",
sessionStorage: customSessionStorage,
distribution: AppDistribution.AppStore,
future: {
unstable_newEmbeddedAuthStrategy: true,
},
});

OAuth Flow

Required Scopes

  • read_customer_events - Read web pixel tracking data
  • write_pixels - Create and manage web pixel extensions
  • write_products - Create and update product data

Session Storage

RecoApp uses a custom session storage implementation that extends Prisma's session storage:

Location: recoapp-shopify/app/session-storage.server.ts

Database Schema:

model Session {
id String @id
shop String
state String
isOnline Boolean @default(false)
scope String?
expires DateTime?
accessToken String
userId BigInt?
firstName String?
lastName String?
email String?
accountOwner Boolean @default(false)
locale String?
collaborator Boolean? @default(false)
emailVerified Boolean? @default(false)
}
warning

Session ID Sanitization: Session IDs have periods replaced with underscores for Xata database compatibility:

function sanitizeId(id: string): string {
return id.replace(/\./g, "_");
}

This is required because Xata doesn't allow periods in record IDs.

Access Token Encryption

Access tokens are sent as plaintext over HTTPS to recoapp-api, which handles server-side encryption.

Location: recoapp-api/src/shops/shops.service.ts (Lines 340-359)

Encryption Method:

  • Algorithm: AES-256-CBC
  • Key Length: 32 characters (256 bits)
  • Random IV: 16 bytes per encryption
  • Storage Format: {iv-hex}:{encrypted-hex}
private encryptToken(token: string): string {
const key = Buffer.from(ENCRYPTION_KEY, 'utf8'); // 32 bytes
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
const encrypted = Buffer.concat([
cipher.update(token, 'utf8'),
cipher.final(),
]);
return `${iv.toString('hex')}:${encrypted.toString('hex')}`;
}
tip

Generate a secure encryption key:

openssl rand -hex 32

Set this as ENCRYPTION_KEY in your recoapp-api .env file.


2. Session Token Authentication (JWT)

Purpose

Authenticates requests from the Shopify embedded app frontend to the recoapp-api backend.

Implementation

Guard Location: recoapp-api/src/shopify/guards/shopify-auth.guard.ts

Service Location: recoapp-api/src/shopify/shopify-auth.service.ts

How It Works

  1. Token Generation: Shopify App Bridge generates session tokens (JWTs) in the embedded app frontend
  2. Request: Frontend includes token in Authorization: Bearer {token} header
  3. Validation: ShopifyAuthGuard validates the JWT signature and claims
  4. Shop Lookup: Guard looks up the shop in the database
  5. Context Storage: Authenticated shop stored in request-scoped context

JWT Structure

{
iss: "https://shop.myshopify.com/admin", // Issuer
dest: "https://shop.myshopify.com", // Destination
aud: "<SHOPIFY_API_KEY>", // Audience
sub: "<shopify-user-id>", // Subject
exp: 1234567890, // Expiration (60 sec)
nbf: 1234567830, // Not before
iat: 1234567830, // Issued at
jti: "<unique-id>", // JWT ID
sid: "<session-id>" // Session ID
}

Validation Steps

Location: recoapp-api/src/shopify/guards/shopify-auth.guard.ts (Lines 45-97)

  1. Extract token from Authorization header
  2. Verify JWT signature using SHOPIFY_API_SECRET
  3. Validate audience matches SHOPIFY_API_KEY
  4. Validate issuer is from .myshopify.com/admin
  5. Validate destination hostname matches issuer
  6. Extract shop domain from dest claim
  7. Look up shop in database
  8. Verify shop is not uninstalled
  9. Store shop in request-scoped context
  10. Return success
warning

Token Expiry: Session tokens expire after 60 seconds. The guard adds response headers to help frontends manage token refresh:

  • X-Token-Expiry: Expiration timestamp
  • X-Token-Age: Age in seconds
  • X-Time-To-Expiry: Seconds remaining
  • X-Token-Refresh-Required: Set to 'true' if < 15 seconds remain

Usage Example

@Get('/shops/me')
@UseGuards(ShopifyAuthGuard)
async getCurrentShop(@CurrentShop() shop: ShopRecord) {
return shop;
}

Security Features

  • JWT signature verification (HMAC-SHA256)
  • Cross-claim validation (iss/dest consistency)
  • Uninstalled shop rejection
  • Request-scoped context (prevents shop leakage)
  • Generic error messages (don't leak shop existence)

3. Pixel API Key Authentication

Purpose

Authenticates pixel tracking events sent from Shopify storefronts to prevent cross-shop data pollution and unauthorised tracking.

Implementation

Guard Location: recoapp-api/src/pixels/guards/pixel-auth.guard.ts

API Key Format

{shop_id}_{64-character-hex-string}

Example:

gid://shopify/Shop/87722238222_a1b2c3d4e5f6789abcdef0123456789abcdef0123456789abcdef0123456789

Key Generation

Location: recoapp-api/src/shops/shops.service.ts (Lines 36-43)

private generateRecoAppApiKey(shopId: string): string {
const randomBytes = crypto.randomBytes(32).toString('hex');
return `${shopId}_${randomBytes}`;
}

Keys are automatically generated when a shop is created/installed and returned in the API response.

How It Works

Validation Steps

Location: recoapp-api/src/pixels/guards/pixel-auth.guard.ts (Lines 42-90)

  1. Extract X-Shop-API-Key from request header
  2. Extract shop_id from request body
  3. Validate both credentials are present
  4. Look up shop by recoapp_api_key in database
  5. Verify shop exists
  6. Verify shop_id matches the shop linked to the API key
  7. Verify shop is not uninstalled
  8. Allow request to proceed
warning

Cross-Shop Protection: The guard explicitly validates that the shop_id in the request body matches the shop that owns the API key. This prevents Shop A from using their key to send events for Shop B.

if (shop.shop_id !== shopIdStr) {
throw new UnauthorizedException("shop_id does not match RecoApp API key");
}

Web Pixel Configuration

Location: recoapp-shopify/extensions/recoapp-web-pixel/src/index.ts

const sendEventToAPI = async (endpoint: string, payload: any) => {
const response = await fetch(`${RECOAPP_API_BASE_URL}${endpoint}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Shop-API-Key": recoappApiKey, // From pixel settings
},
body: JSON.stringify({
...payload,
shop_id: shopifyShopId, // From pixel settings
}),
});
};

Pixel Settings Configuration

Location: recoapp-shopify/extensions/recoapp-web-pixel/shopify.extension.toml

[settings.fields.shopify_shop_id]
description = "Shopify Shop ID"
name = "Shopify Shop ID"
type = "single_line_text_field"

[settings.fields.recoapp_api_key]
description = "RecoApp API Key for authentication"
name = "RecoApp API Key"
type = "single_line_text_field"
validations = [{name = "min", value = "32"}]

These settings must be configured in the Shopify Admin after pixel installation.

tip

Obtaining the API Key: When a shop is created via POST /shops, the response includes the recoappApiKey field. This value should be configured in the web pixel settings.


4. Internal API Key Authentication

Purpose

Authenticates server-to-server communication between recoapp-shopify and recoapp-api, specifically for shop creation during installation.

Implementation

Guard Location: recoapp-api/src/common/guards/api-key.guard.ts

How It Works

Validation

Location: recoapp-api/src/common/guards/api-key.guard.ts (Lines 38-75)

const apiKey = request.headers["x-api-key"];
const expectedKey = this.configService.get<string>("INTERNAL_API_KEY");

if (!apiKey || !this.secureCompare(apiKey, expectedKey)) {
throw new UnauthorizedException("Invalid API key");
}
warning

Timing Attack Prevention: The guard uses constant-time comparison to prevent timing attacks:

private secureCompare(a: string, b: string): boolean {
if (a.length !== b.length) return false;

let result = 0;
for (let i = 0; i < a.length; i++) {
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
}

return result === 0;
}

Usage in Shopify App

Location: recoapp-shopify/app/api/apiShop.server.ts (Lines 154-195)

const response = await fetch(`${apiUrl}/shops/`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": process.env.RECOAPP_INTERNAL_API_KEY,
},
body: JSON.stringify(shopData),
});
tip

Key Generation: Generate a secure internal API key:

openssl rand -hex 32

Set as INTERNAL_API_KEY in recoapp-api .env and RECOAPP_INTERNAL_API_KEY in recoapp-shopify .env. Both values must match.

Protected Endpoints

Currently, only the shop creation endpoint is protected by this guard:

@Post('/shops')
@UseGuards(ApiKeyGuard)
async create(@Body() createShopDto: CreateShopDto) {
return await this.shopsService.createOne(createShopDto);
}

5. Webhook HMAC Validation

Purpose

Validates that incoming webhooks are genuinely from Shopify and prevents replay attacks.

Implementation

Guard Location: recoapp-api/src/shopify/guards/shopify-webhook.guard.ts

Validation Process

HMAC Validation

Location: recoapp-api/src/shopify/guards/shopify-webhook.guard.ts (Lines 49-57)

const hmac = crypto
.createHmac("sha256", SHOPIFY_API_SECRET)
.update(rawBody, "utf8")
.digest("base64");

if (!this.secureCompare(hmac, receivedHmac)) {
throw new UnauthorizedException("Invalid HMAC signature");
}

Timestamp Validation

Webhooks must be recent to prevent replay attacks:

const webhookAge = Date.now() - webhookTimestamp;
const maxAge = 5 * 60 * 1000; // 5 minutes

if (webhookAge > maxAge) {
throw new UnauthorizedException("Webhook timestamp too old");
}

Replay Attack Prevention

Location: recoapp-api/src/shopify/guards/shopify-webhook.guard.ts (Lines 77-91)

const webhookId = request.headers["x-shopify-webhook-id"];

if (webhookId && this.recentWebhooks.has(webhookId)) {
throw new UnauthorizedException("Duplicate webhook ID");
}

this.recentWebhooks.add(webhookId);

// Clean up after 10 minutes
setTimeout(() => {
this.recentWebhooks.delete(webhookId);
}, 10 * 60 * 1000);
warning

Production Limitation: The webhook ID cache is currently in-memory (Set<string>), which works for single-instance deployments but won't prevent duplicate processing in multi-instance environments.

Recommendation: Migrate to Redis-backed deduplication for production:

// Check if webhook ID exists in Redis
const exists = await redis.exists(`webhook:${webhookId}`);
if (exists) throw new UnauthorizedException("Duplicate request");

// Store webhook ID with 10-minute TTL
await redis.setex(`webhook:${webhookId}`, 600, "1");

Security Features

  • HMAC-SHA256 signature verification
  • Constant-time comparison (prevents timing attacks)
  • Timestamp validation (5-minute window)
  • Webhook ID deduplication (10-minute cache)
  • Clock skew tolerance (1-minute future tolerance)
  • Raw body preservation (required for HMAC)

6. Super Admin Authentication (Development Only)

Purpose

Allows developers to bypass normal authentication and impersonate any shop for testing purposes.

danger

NEVER enable this feature in production! It bypasses all authentication and authorisation checks.

Configuration

Location: recoapp-api/.env

SUPER_ADMIN_ENABLED=true
SUPER_ADMIN_API_KEY=<generate-with-openssl-rand-hex-32>

Usage

Headers:

X-Super-Admin-Key: <your-super-admin-api-key>
X-Impersonate-Shop: <shop_id or myshopify_domain>

Examples:

X-Impersonate-Shop: gid://shopify/Shop/87722238222
X-Impersonate-Shop: dev-store.myshopify.com

Implementation

Location: recoapp-api/src/shopify/guards/shopify-auth.guard.ts (Lines 121-201)

The guard attempts super admin authentication before falling back to normal session token validation:

// 1. Try super admin authentication
if (this.isSuperAdminEnabled() && this.hasSuperAdminHeaders(request)) {
return await this.authenticateAsSuperAdmin(request, response);
}

// 2. Fall back to normal session token authentication
return await this.authenticateWithSessionToken(request, response);

Response Headers

When super admin is active, these headers are added:

X-Super-Admin-Active: true
X-Impersonated-Shop: dev-store.myshopify.com
X-Impersonated-Shop-Id: gid://shopify/Shop/87722238222

Security Features

  • Disabled by default (SUPER_ADMIN_ENABLED=false)
  • Requires explicit configuration (both flag + API key)
  • 32+ character key requirement
  • Constant-time key comparison
  • All activity logged at WARN level (audit trail)
  • Response headers show super admin is active (transparency)
  • Can impersonate uninstalled shops for testing
tip

Postman Testing: Super admin is particularly useful for testing API endpoints in Postman without needing valid Shopify session tokens.

See docs-temp/SUPER_ADMIN_QUICK_START.md for a quick-start guide.


Environment Variables

recoapp-shopify

# Shopify OAuth
SHOPIFY_API_KEY=<from-partner-dashboard>
SHOPIFY_API_SECRET=<from-partner-dashboard>
SHOPIFY_APP_URL=<app-url> # Dynamic in dev, static in prod
SCOPES=read_customer_events,write_pixels,write_products

# Database
DATABASE_URL=<postgresql-connection-string>

# RecoApp API Integration
RECOAPP_INTERNAL_API_KEY=<must-match-api> # Min 32 chars

# Optional
SHOP_CUSTOM_DOMAIN=<custom-domain-if-any>

recoapp-api

# Shopify
SHOPIFY_API_KEY=<must-match-shopify-app>
SHOPIFY_API_SECRET=<must-match-shopify-app>
SHOPIFY_APP_URL=<api-base-url>
SHOPIFY_SCOPES=read_customer_events,write_pixels

# Database
XATA_API_KEY=<from-xata-dashboard>
DATABASE_URL=<xata-database-url>
XATA_BRANCH=main # or dev

# Security
ENCRYPTION_KEY=<32-characters-exactly> # openssl rand -hex 32
INTERNAL_API_KEY=<must-match-shopify-app> # Min 32 chars

# Development Only (NEVER in production)
SUPER_ADMIN_ENABLED=false # Set to true only in dev
SUPER_ADMIN_API_KEY=<generate-if-enabled> # Min 32 chars

Testing Authentication

Session Token Authentication

curl -X GET https://api.recoapp.com/shops/me \
-H "Authorization: Bearer <session-token>" \
-H "Content-Type: application/json"

Pixel Authentication

curl -X POST https://api.recoapp.com/pixels/product-viewed \
-H "X-Shop-API-Key: <recoapp-api-key>" \
-H "Content-Type: application/json" \
-d '{"shop_id": "gid://shopify/Shop/12345", ...}'

Internal API Key

curl -X POST https://api.recoapp.com/shops \
-H "X-API-Key: <internal-api-key>" \
-H "Content-Type: application/json" \
-d '{...shop-data...}'

Super Admin (Development Only)

curl -X GET https://api.recoapp.com/shops/me \
-H "X-Super-Admin-Key: <super-admin-key>" \
-H "X-Impersonate-Shop: dev-store.myshopify.com"

Security Best Practises

Key Management

  1. Generate Strong Keys:

    openssl rand -hex 32  # For ENCRYPTION_KEY, INTERNAL_API_KEY, SUPER_ADMIN_API_KEY
  2. Environment-Specific Keys:

    • Use different keys for development, staging, and production
    • Never reuse keys across environments
  3. Key Storage:

    • Store in environment variables only
    • Never commit to version control
    • Use secret management services in production (HashiCorp Vault, AWS Secrets Manager, etc.)
  4. Key Rotation:

    • Rotate keys quarterly
    • Document rotation procedures
    • Support multiple active keys during rotation period

Token Security

  1. HTTPS Enforcement:

    • All production environments must use HTTPS
    • Session tokens transmitted over HTTPS only
  2. Token Expiry:

    • Session tokens expire after 60 seconds
    • Implement frontend token refresh logic
    • Monitor token age via response headers
  3. Encrypted Storage:

    • Access tokens encrypted with AES-256-CBC
    • Random IV per encryption
    • Never store plaintext tokens

Webhook Security

  1. HMAC Validation:

    • Always validate HMAC signatures
    • Use constant-time comparison
    • Reject invalid signatures immediately
  2. Replay Protection:

    • Validate webhook timestamps
    • Deduplicate webhook IDs
    • Use Redis in production for distributed deduplication
  3. Raw Body Preservation:

    • HMAC validation requires raw request body
    • Configure body parser middleware correctly

Troubleshooting

"Authentication required"

  • Check: Authorization header is present and formatted correctly
  • Check: Session token is not expired (tokens last 60 seconds)
  • Check: Shop exists in database and is not uninstalled

"Invalid API key"

  • Check: Environment variables are set correctly
  • Check: Keys match between recoapp-shopify and recoapp-api
  • Check: Keys are minimum 32 characters

"ENCRYPTION_KEY must be exactly 32 characters"

  • Check: ENCRYPTION_KEY in recoapp-api .env is exactly 32 characters
  • Generate: openssl rand -hex 32 produces 64 characters, use only first 32

"Invalid HMAC signature"

  • Check: SHOPIFY_API_SECRET matches between app and API
  • Check: Raw body middleware is configured correctly
  • Check: Body parser is not modifying request body before HMAC validation

"shop_id does not match RecoApp API key"

  • Check: shop_id in pixel event matches the shop that owns the API key
  • Check: Pixel settings are configured with correct shopify_shop_id
  • Check: API key belongs to the shop sending the event

Further Reading