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 Method | Use Case | Components |
|---|---|---|
| Shopify OAuth 2.0 | Merchant installation and app access | recoapp-shopify |
| Session Token (JWT) | Admin API requests from embedded app | recoapp-api |
| Pixel API Keys | Storefront tracking events | recoapp-api, web pixel extension |
| Internal API Keys | Server-to-server communication | recoapp-shopify ↔ recoapp-api |
| Webhook HMAC | Shopify webhook verification | recoapp-api |
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 datawrite_pixels- Create and manage web pixel extensionswrite_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)
}
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')}`;
}
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
- Token Generation: Shopify App Bridge generates session tokens (JWTs) in the embedded app frontend
- Request: Frontend includes token in
Authorization: Bearer {token}header - Validation: ShopifyAuthGuard validates the JWT signature and claims
- Shop Lookup: Guard looks up the shop in the database
- 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)
- Extract token from
Authorizationheader - Verify JWT signature using
SHOPIFY_API_SECRET - Validate audience matches
SHOPIFY_API_KEY - Validate issuer is from
.myshopify.com/admin - Validate destination hostname matches issuer
- Extract shop domain from
destclaim - Look up shop in database
- Verify shop is not uninstalled
- Store shop in request-scoped context
- Return success
Token Expiry: Session tokens expire after 60 seconds. The guard adds response headers to help frontends manage token refresh:
X-Token-Expiry: Expiration timestampX-Token-Age: Age in secondsX-Time-To-Expiry: Seconds remainingX-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)
- Extract
X-Shop-API-Keyfrom request header - Extract
shop_idfrom request body - Validate both credentials are present
- Look up shop by
recoapp_api_keyin database - Verify shop exists
- Verify
shop_idmatches the shop linked to the API key - Verify shop is not uninstalled
- Allow request to proceed
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.
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");
}
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),
});
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);
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.
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
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
-
Generate Strong Keys:
openssl rand -hex 32 # For ENCRYPTION_KEY, INTERNAL_API_KEY, SUPER_ADMIN_API_KEY -
Environment-Specific Keys:
- Use different keys for development, staging, and production
- Never reuse keys across environments
-
Key Storage:
- Store in environment variables only
- Never commit to version control
- Use secret management services in production (HashiCorp Vault, AWS Secrets Manager, etc.)
-
Key Rotation:
- Rotate keys quarterly
- Document rotation procedures
- Support multiple active keys during rotation period
Token Security
-
HTTPS Enforcement:
- All production environments must use HTTPS
- Session tokens transmitted over HTTPS only
-
Token Expiry:
- Session tokens expire after 60 seconds
- Implement frontend token refresh logic
- Monitor token age via response headers
-
Encrypted Storage:
- Access tokens encrypted with AES-256-CBC
- Random IV per encryption
- Never store plaintext tokens
Webhook Security
-
HMAC Validation:
- Always validate HMAC signatures
- Use constant-time comparison
- Reject invalid signatures immediately
-
Replay Protection:
- Validate webhook timestamps
- Deduplicate webhook IDs
- Use Redis in production for distributed deduplication
-
Raw Body Preservation:
- HMAC validation requires raw request body
- Configure body parser middleware correctly
Troubleshooting
"Authentication required"
- Check:
Authorizationheader 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_KEYin recoapp-api.envis exactly 32 characters - Generate:
openssl rand -hex 32produces 64 characters, use only first 32
"Invalid HMAC signature"
- Check:
SHOPIFY_API_SECRETmatches 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_idin 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
Related Documentation
- Authorisation - CASL-based authorisation and shop-scoped permissions
- Installation: recoapp-api - API setup and configuration
- Installation: recoapp-shopify - Shopify app setup