# Authentication & Security

## Overview

The EventBookings Shopify app implements multiple layers of security including OAuth 2.0 flows, CSRF protection, CORS handling, and secure token management. This document provides comprehensive implementation details for all security aspects.

## Authentication Flows

### 1. Shopify OAuth 2.0 Flow

The app uses Shopify's OAuth 2.0 implementation for store authentication and authorization.

#### Flow Diagram
```
Store Owner → Install App → Shopify OAuth → App Authorization → Access Token → Dashboard Access
```

#### Implementation

**OAuth Controller (Laravel Example)**:
```php
<?php

namespace App\Http\Controllers;

use App\Models\ShopifyStore;
use App\Services\ShopifyAuth;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;

class ShopifyAuthController extends Controller
{
    private $shopifyAuth;

    public function __construct(ShopifyAuth $shopifyAuth)
    {
        $this->shopifyAuth = $shopifyAuth;
    }

    /**
     * Handle app installation request
     */
    public function install(Request $request)
    {
        $shop = $request->query('shop');

        if (!$shop) {
            return response('Missing shop parameter', 400);
        }

        // Validate shop domain format
        if (!$this->isValidShopDomain($shop)) {
            return response('Invalid shop domain', 400);
        }

        // Generate state token for CSRF protection
        $state = bin2hex(random_bytes(32));
        session(['oauth_state' => $state]);

        // Required scopes for the app
        $scopes = [
            'read_themes',
            'write_themes', // For theme integration
            // Add other required scopes
        ];

        $authUrl = $this->shopifyAuth->getAuthUrl($shop, $scopes, $state);

        Log::info('OAuth installation initiated', [
            'shop' => $shop,
            'state' => $state
        ]);

        return redirect($authUrl);
    }

    /**
     * Handle OAuth callback
     */
    public function callback(Request $request)
    {
        $shop = $request->query('shop');
        $code = $request->query('code');
        $state = $request->query('state');
        $hmac = $request->query('hmac');

        // Validate required parameters
        if (!$shop || !$code || !$state) {
            Log::warning('OAuth callback missing parameters', [
                'shop' => $shop,
                'has_code' => !empty($code),
                'has_state' => !empty($state)
            ]);
            return response('Missing required parameters', 400);
        }

        // Verify state to prevent CSRF attacks
        if (!hash_equals(session('oauth_state', ''), $state)) {
            Log::warning('OAuth state mismatch', [
                'shop' => $shop,
                'expected' => session('oauth_state'),
                'received' => $state
            ]);
            return response('Invalid state parameter', 400);
        }

        // Verify HMAC signature
        if (!$this->verifyHmac($request->query())) {
            Log::warning('OAuth HMAC verification failed', [
                'shop' => $shop
            ]);
            return response('Invalid request signature', 400);
        }

        try {
            // Exchange code for access token
            $accessToken = $this->shopifyAuth->getAccessToken($shop, $code);

            // Store or update shop information
            $store = ShopifyStore::updateOrCreate(
                ['shop_domain' => $shop],
                ['access_token' => $accessToken]
            );

            // Create default store settings
            $store->storeSettings()->firstOrCreate([
                'store_id' => $store->id
            ]);

            Log::info('OAuth flow completed successfully', [
                'shop' => $shop,
                'store_id' => $store->id
            ]);

            // Clear OAuth state
            session()->forget('oauth_state');

            // Redirect to dashboard
            return redirect()->route('dashboard', ['shop' => $shop])
                           ->with('success', 'App installed successfully!');

        } catch (\Exception $e) {
            Log::error('OAuth callback failed', [
                'shop' => $shop,
                'error' => $e->getMessage()
            ]);

            return response('Installation failed: ' . $e->getMessage(), 500);
        }
    }

    /**
     * Verify HMAC signature for OAuth callback
     */
    private function verifyHmac(array $queryParams): bool
    {
        $hmac = $queryParams['hmac'] ?? '';
        unset($queryParams['hmac'], $queryParams['signature']);

        // Sort parameters and build query string
        ksort($queryParams);
        $queryString = http_build_query($queryParams);

        // Calculate expected HMAC
        $expectedHmac = hash_hmac('sha256', $queryString, config('shopify.client_secret'));

        return hash_equals($hmac, $expectedHmac);
    }

    /**
     * Validate shop domain format
     */
    private function isValidShopDomain(string $shop): bool
    {
        // Basic validation for Shopify domain format
        return preg_match('/^[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9]\.myshopify\.com$/', $shop) ||
               preg_match('/^[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9]$/', $shop);
    }
}
```

#### Shopify Auth Service

```php
<?php

namespace App\Services;

use GuzzleHttp\Client;
use Illuminate\Support\Facades\Log;

class ShopifyAuth
{
    private const API_VERSION = '2023-07';

    private $clientId;
    private $clientSecret;
    private $redirectUri;
    private $httpClient;

    public function __construct()
    {
        $this->clientId = config('shopify.client_id');
        $this->clientSecret = config('shopify.client_secret');
        $this->redirectUri = config('shopify.redirect_uri');
        $this->httpClient = new Client(['timeout' => 30]);
    }

    /**
     * Generate OAuth authorization URL
     */
    public function getAuthUrl(string $shop, array $scopes = [], string $state = null): string
    {
        $shop = $this->normalizeShopDomain($shop);

        $params = [
            'client_id' => $this->clientId,
            'scope' => implode(',', $scopes),
            'redirect_uri' => $this->redirectUri,
        ];

        if ($state) {
            $params['state'] = $state;
        }

        return "https://{$shop}/admin/oauth/authorize?" . http_build_query($params);
    }

    /**
     * Exchange authorization code for access token
     */
    public function getAccessToken(string $shop, string $code): string
    {
        $shop = $this->normalizeShopDomain($shop);

        try {
            $response = $this->httpClient->post("https://{$shop}/admin/oauth/access_token", [
                'form_params' => [
                    'client_id' => $this->clientId,
                    'client_secret' => $this->clientSecret,
                    'code' => $code,
                ]
            ]);

            $data = json_decode($response->getBody()->getContents(), true);

            if (!isset($data['access_token'])) {
                throw new \RuntimeException('No access token in response');
            }

            Log::info('Successfully obtained Shopify access token', [
                'shop' => $shop
            ]);

            return $data['access_token'];

        } catch (\Exception $e) {
            Log::error('Failed to get Shopify access token', [
                'shop' => $shop,
                'error' => $e->getMessage()
            ]);

            throw new \RuntimeException('Failed to get access token: ' . $e->getMessage());
        }
    }

    /**
     * Verify webhook authenticity
     */
    public function verifyWebhook(string $data, string $hmacHeader): bool
    {
        $calculatedHmac = base64_encode(hash_hmac('sha256', $data, $this->clientSecret, true));

        return hash_equals($hmacHeader, $calculatedHmac);
    }

    /**
     * Normalize shop domain format
     */
    private function normalizeShopDomain(string $shop): string
    {
        $shop = strtolower(trim($shop));
        $shop = preg_replace('/^https?:\/\//', '', $shop);

        if (!str_ends_with($shop, '.myshopify.com')) {
            $shop .= '.myshopify.com';
        }

        return $shop;
    }
}
```

### 2. EventBookings OAuth 2.0 Client Credentials Flow

Implementation covered in [API Integrations](03-api-integrations.md#eventbookings-api-integration).

## Middleware Security

### 1. Shopify Authentication Middleware

```php
<?php

namespace App\Http\Middleware;

use App\Models\ShopifyStore;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;

class VerifyShopifyAuth
{
    /**
     * Handle an incoming request
     */
    public function handle(Request $request, Closure $next)
    {
        $shop = $request->query('shop') ?: $request->header('X-Shop-Domain');

        if (!$shop) {
            Log::warning('Request missing shop parameter');
            return response('Unauthorized - Missing shop parameter', 401);
        }

        // Find store in database
        $store = ShopifyStore::where('shop_domain', $shop)->first();

        if (!$store) {
            Log::warning('Unknown shop attempted access', ['shop' => $shop]);
            return response('Unauthorized - Shop not found', 401);
        }

        // Verify access token is still valid (optional - add API call to Shopify)
        // This can be expensive, so consider caching the validation result

        // Add store to request for use in controllers
        $request->attributes->set('store', $store);

        return $next($request);
    }
}
```

### 2. CORS Middleware for Widget Requests

```php
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;

class CorsMiddleware
{
    /**
     * Handle CORS for widget API endpoints
     */
    public function handle(Request $request, Closure $next)
    {
        // Handle preflight OPTIONS requests
        if ($request->getMethod() === 'OPTIONS') {
            return $this->handlePreflightRequest($request);
        }

        $response = $next($request);

        return $this->addCorsHeaders($response, $request);
    }

    /**
     * Handle preflight OPTIONS request
     */
    private function handlePreflightRequest(Request $request)
    {
        $response = response('', 204);
        return $this->addCorsHeaders($response, $request);
    }

    /**
     * Add CORS headers to response
     */
    private function addCorsHeaders($response, Request $request)
    {
        $origin = $request->header('Origin');

        // Validate origin for widget requests
        if ($this->isValidWidgetOrigin($origin, $request)) {
            $response->header('Access-Control-Allow-Origin', $origin);
        } else {
            // Fallback for development or if origin validation fails
            $response->header('Access-Control-Allow-Origin', '*');

            Log::warning('Widget request from unvalidated origin', [
                'origin' => $origin,
                'shop' => $request->query('shop'),
                'user_agent' => $request->header('User-Agent')
            ]);
        }

        $response->header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, HEAD');
        $response->header('Access-Control-Allow-Headers', 'Content-Type, Origin, Accept, X-Shop-Domain, X-Requested-With, Authorization, Cache-Control');
        $response->header('Access-Control-Max-Age', '3600');
        $response->header('Access-Control-Allow-Credentials', 'false');

        return $response;
    }

    /**
     * Validate origin for widget requests
     */
    private function isValidWidgetOrigin(?string $origin, Request $request): bool
    {
        if (!$origin) {
            return false;
        }

        try {
            $parsed = parse_url($origin);
            $domain = strtolower($parsed['host'] ?? '');

            // Must be HTTPS in production
            if (!config('app.debug') && ($parsed['scheme'] ?? '') !== 'https') {
                return false;
            }

            // Allow localhost in debug mode
            if (config('app.debug') && str_contains($domain, 'localhost')) {
                return true;
            }

            // Check if it's a known Shopify domain pattern
            if (str_ends_with($domain, '.myshopify.com')) {
                $shopName = str_replace('.myshopify.com', '', $domain);
                // Basic validation of shop name format
                if (preg_match('/^[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9]$/', $shopName)) {
                    return true;
                }
            }

            // Check against registered store domains
            $shop = $request->query('shop');
            if ($shop) {
                $store = \App\Models\ShopifyStore::where('shop_domain', $shop)->first();
                if ($store) {
                    // Could implement custom domain checking here
                    return true;
                }
            }

            return false;

        } catch (\Exception $e) {
            Log::error('Error validating widget origin', [
                'origin' => $origin,
                'error' => $e->getMessage()
            ]);
            return false;
        }
    }
}
```

### 3. Webhook Verification Middleware

```php
<?php

namespace App\Http\Middleware;

use App\Services\ShopifyAuth;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;

class VerifyShopifyWebhook
{
    private $shopifyAuth;

    public function __construct(ShopifyAuth $shopifyAuth)
    {
        $this->shopifyAuth = $shopifyAuth;
    }

    /**
     * Verify Shopify webhook authenticity
     */
    public function handle(Request $request, Closure $next)
    {
        $hmacHeader = $request->header('X-Shopify-Hmac-Sha256');
        $data = $request->getContent();

        if (!$hmacHeader) {
            Log::warning('Webhook request missing HMAC header');
            return response('Unauthorized', 401);
        }

        if (!$this->shopifyAuth->verifyWebhook($data, $hmacHeader)) {
            Log::warning('Webhook HMAC verification failed', [
                'shop' => $request->header('X-Shopify-Shop-Domain'),
                'topic' => $request->header('X-Shopify-Topic')
            ]);
            return response('Unauthorized', 401);
        }

        Log::info('Webhook verified successfully', [
            'shop' => $request->header('X-Shopify-Shop-Domain'),
            'topic' => $request->header('X-Shopify-Topic')
        ]);

        return $next($request);
    }
}
```

## CSRF Protection

### Implementation

```php
<?php

namespace App\Http\Controllers;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Request;

class DashboardController extends Controller
{
    public function __construct()
    {
        // Apply CSRF protection to all POST requests except API endpoints
        $this->middleware('auth.shopify');
        $this->middleware('csrf')->except(['widgetApi', 'webhookHandler']);
    }

    /**
     * Display dashboard with CSRF token
     */
    public function index(Request $request)
    {
        $store = $request->attributes->get('store');

        return view('dashboard', [
            'store' => $store,
            'csrf_token' => csrf_token(),
            'shop_domain' => $store->shop_domain,
            // ... other data
        ]);
    }

    /**
     * Handle settings update with CSRF protection
     */
    public function updateSettings(DashboardRequest $request)
    {
        // CSRF automatically verified by DashboardRequest
        $store = $request->attributes->get('store');

        // Process settings update
        // ...

        return back()->with('success', 'Settings updated successfully');
    }
}
```

### Custom Form Request for Enhanced Validation

```php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class DashboardRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request
     */
    public function authorize(): bool
    {
        // Additional authorization logic if needed
        return $this->attributes->has('store');
    }

    /**
     * Get the validation rules that apply to the request
     */
    public function rules(): array
    {
        $action = $this->input('action');

        switch ($action) {
            case 'save':
                return [
                    'client_id' => 'required|string|max:255',
                    'client_secret' => 'required|string|max:255',
                ];

            case 'update_toggle':
                return [
                    'event_uuid' => 'required|string|max:255',
                    'toggle_type' => 'required|in:show_event,featured_event',
                    'value' => 'required|in:0,1',
                ];

            default:
                return [];
        }
    }

    /**
     * Configure the validator instance
     */
    public function withValidator($validator)
    {
        $validator->after(function ($validator) {
            // Additional custom validation logic
            $this->validateShopOwnership($validator);
        });
    }

    /**
     * Validate that the request belongs to the authenticated shop
     */
    private function validateShopOwnership($validator)
    {
        $store = $this->attributes->get('store');
        $shopParam = $this->query('shop') ?: $this->input('shop');

        if ($shopParam && $store->shop_domain !== $shopParam) {
            $validator->errors()->add('shop', 'Shop parameter does not match authenticated store');
        }
    }
}
```

## Secure Token Storage

### Database Encryption

```php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Crypt;

class ShopifyStore extends Model
{
    protected $fillable = [
        'shop_domain',
        'access_token'
    ];

    protected $hidden = [
        'access_token'
    ];

    /**
     * Encrypt access token before storing
     */
    public function setAccessTokenAttribute($value)
    {
        $this->attributes['access_token'] = Crypt::encryptString($value);
    }

    /**
     * Decrypt access token when retrieving
     */
    public function getAccessTokenAttribute($value)
    {
        try {
            return Crypt::decryptString($value);
        } catch (\Exception $e) {
            \Log::error('Failed to decrypt access token', [
                'store_id' => $this->id,
                'error' => $e->getMessage()
            ]);
            return null;
        }
    }
}

class StoreSettings extends Model
{
    protected $fillable = [
        'store_id',
        'client_id',
        'client_secret',
        'access_token',
        'org_uuid',
        'token_expires_at',
        'is_connected',
        'connection_error'
    ];

    protected $hidden = [
        'client_secret',
        'access_token'
    ];

    protected $casts = [
        'token_expires_at' => 'datetime',
        'is_connected' => 'boolean',
    ];

    /**
     * Encrypt client secret before storing
     */
    public function setClientSecretAttribute($value)
    {
        $this->attributes['client_secret'] = $value ? Crypt::encryptString($value) : '';
    }

    /**
     * Decrypt client secret when retrieving
     */
    public function getClientSecretAttribute($value)
    {
        if (!$value) return '';

        try {
            return Crypt::decryptString($value);
        } catch (\Exception $e) {
            \Log::error('Failed to decrypt client secret', [
                'store_id' => $this->store_id,
                'error' => $e->getMessage()
            ]);
            return '';
        }
    }

    /**
     * Encrypt EventBookings access token before storing
     */
    public function setAccessTokenAttribute($value)
    {
        $this->attributes['access_token'] = $value ? Crypt::encryptString($value) : '';
    }

    /**
     * Decrypt EventBookings access token when retrieving
     */
    public function getAccessTokenAttribute($value)
    {
        if (!$value) return '';

        try {
            return Crypt::decryptString($value);
        } catch (\Exception $e) {
            \Log::error('Failed to decrypt EventBookings access token', [
                'store_id' => $this->store_id,
                'error' => $e->getMessage()
            ]);
            return '';
        }
    }
}
```

## Input Validation and Sanitization

### Enhanced Validation Rules

```php
<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;

class ShopifyDomain implements Rule
{
    /**
     * Determine if the validation rule passes
     */
    public function passes($attribute, $value): bool
    {
        if (!is_string($value)) {
            return false;
        }

        $value = strtolower(trim($value));

        // Remove protocol if present
        $value = preg_replace('/^https?:\/\//', '', $value);

        // Check if it's a valid Shopify domain format
        return preg_match('/^[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9]\.myshopify\.com$/', $value) ||
               preg_match('/^[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9]$/', $value);
    }

    /**
     * Get the validation error message
     */
    public function message(): string
    {
        return 'The :attribute must be a valid Shopify domain.';
    }
}

class EventUuid implements Rule
{
    /**
     * Determine if the validation rule passes
     */
    public function passes($attribute, $value): bool
    {
        // UUID v4 format validation
        return is_string($value) &&
               preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i', $value);
    }

    /**
     * Get the validation error message
     */
    public function message(): string
    {
        return 'The :attribute must be a valid UUID.';
    }
}
```

### Usage in Requests

```php
use App\Rules\ShopifyDomain;
use App\Rules\EventUuid;

class DashboardRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'shop' => ['required', new ShopifyDomain],
            'event_uuid' => ['required', new EventUuid],
            'client_id' => 'required|string|max:255|regex:/^[a-zA-Z0-9_-]+$/',
            'client_secret' => 'required|string|max:255|regex:/^[a-zA-Z0-9_-]+$/',
        ];
    }
}
```

## Security Headers

### Security Headers Middleware

```php
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class SecurityHeaders
{
    /**
     * Add security headers to response
     */
    public function handle(Request $request, Closure $next)
    {
        $response = $next($request);

        // Content Security Policy for embedded apps
        $csp = [
            "default-src 'self'",
            "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.shopify.com https://unpkg.com",
            "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net",
            "img-src 'self' data: https:",
            "font-src 'self' https://cdn.jsdelivr.net",
            "connect-src 'self' https://api-rto.eventbookings.com https://identity.eventbookings.com",
            "frame-ancestors https://*.myshopify.com https://admin.shopify.com",
        ];

        $response->header('Content-Security-Policy', implode('; ', $csp));

        // Allow embedding in Shopify admin
        $response->header('X-Frame-Options', 'ALLOWALL');

        // Other security headers
        $response->header('X-Content-Type-Options', 'nosniff');
        $response->header('X-XSS-Protection', '1; mode=block');
        $response->header('Referrer-Policy', 'strict-origin-when-cross-origin');

        // HTTPS enforcement in production
        if (!config('app.debug')) {
            $response->header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
        }

        return $response;
    }
}
```

## Logging and Monitoring

### Security Event Logging

```php
<?php

namespace App\Services;

use Illuminate\Support\Facades\Log;

class SecurityLogger
{
    /**
     * Log authentication events
     */
    public static function logAuth(string $event, array $context = []): void
    {
        Log::channel('security')->info("AUTH: {$event}", $context);
    }

    /**
     * Log security warnings
     */
    public static function logSecurityWarning(string $event, array $context = []): void
    {
        Log::channel('security')->warning("SECURITY: {$event}", $context);
    }

    /**
     * Log security violations
     */
    public static function logSecurityViolation(string $event, array $context = []): void
    {
        Log::channel('security')->error("VIOLATION: {$event}", $context);
    }

    /**
     * Log API access
     */
    public static function logApiAccess(string $api, string $endpoint, array $context = []): void
    {
        Log::channel('api')->info("API: {$api} - {$endpoint}", $context);
    }
}
```

### Usage Example

```php
// In controllers or middleware
use App\Services\SecurityLogger;

// Log successful authentication
SecurityLogger::logAuth('Shopify OAuth completed', [
    'shop' => $shop,
    'ip' => $request->ip(),
    'user_agent' => $request->userAgent()
]);

// Log security warning
SecurityLogger::logSecurityWarning('Invalid origin detected', [
    'origin' => $origin,
    'shop' => $shop,
    'endpoint' => $request->path()
]);

// Log API access
SecurityLogger::logApiAccess('EventBookings', 'fetch_events', [
    'shop' => $shop,
    'events_count' => count($events)
]);
```

This comprehensive security implementation provides multiple layers of protection while maintaining compatibility with Shopify's embedded app requirements and ensuring secure integration with the EventBookings API.