Skip to content

Security, Authentication & Data Protection

The Planovi Microservices implement defense-in-depth security layers to ensure API authorization, secure client-to-service communication, isolated tokenized access, and resilience against replay and caching attacks.


1. Authentication & Authorization Patterns

The microservices utilize three distinct authorization schemes depending on the caller identity and operational context:

flowchart TD
    Req["Incoming HTTP Request"] --> RouteCheck{"Request Destination & Headers"}
    
    RouteCheck -->|"API Request
(e.g., /api/create_offer.php)"| APISecret{"X-Secret-Key Header matches
DEALFLOW_API_SECRET?"} APISecret -->|Valid| ExecuteAPI["Allow API Execution (200 / 201)"] APISecret -->|"Invalid / Missing"| Reject401["Reject with 401 Unauthorized"] RouteCheck -->|"Public Scheduler / Offer
(e.g., offer.php?id=XXX)"| IDSanitize["Sanitize ID Parameter
preg_replace('/[^a-zA-Z0-9_-]/')"] IDSanitize --> CheckFile{"data/{id}.json exists?"} CheckFile -->|Yes| RenderView["Render Dynamic Offer / Calendar View"] CheckFile -->|No| NotFound["Display Safe Not Found View"] RouteCheck -->|"Tokenized Onboarding
(e.g., offer.php?token=XXX)"| CleanToken["Sanitize Token
preg_replace('/[^a-f0-9]/')"] CleanToken --> ExpiryCheck{"Token length >= 32 &
expires_at > now() (72h)?"} ExpiryCheck -->|Active| RenderOnboarding["Render Member Onboarding Form"] ExpiryCheck -->|Expired| TokenExpiredErr["Display Token Expired Notice"]

2. API Secret Key Validation (X-Secret-Key)

Internal endpoints like dealflow/api/create_offer.php and dealflow/api/save_dealflow.php are invoked programmatically by the Planovi Flutter App and Supabase Edge Functions. They require an HTTP request header:

POST /api/create_offer.php HTTP/1.1
Host: dev-dealflow.planovi.app
Content-Type: application/json
X-Secret-Key: <DEALFLOW_API_SECRET>

Verification Implementation

The endpoint safely extracts headers across various server environments (Nginx FastCGI, Apache, PHP-FPM) and verifies against the environment variable DEALFLOW_API_SECRET:

function getRequestHeaders() {
if (function_exists('getallheaders')) return getallheaders();
$headers = [];
foreach ($_SERVER as $key => $value) {
if (substr($key, 0, 5) == 'HTTP_') {
$header = str_replace(' ', '-', ucwords(str_replace('_', ' ', strtolower(substr($key, 5)))));
$headers[$header] = $value;
}
}
return $headers;
}
$headers = getRequestHeaders();
$providedKey = $headers['X-Secret-Key'] ?? ($headers['x-secret-key'] ?? ($_SERVER['HTTP_X_SECRET_KEY'] ?? ''));
if ($providedKey !== SECRET_KEY || empty(SECRET_KEY)) {
http_response_code(401);
echo json_encode([
'status' => 'error',
'error' => 'Unauthorized. Invalid or missing secret key in X-Secret-Key header.'
]);
exit;
}

3. Google OAuth 2.0 Integration & Token Management

The Scheduler integrates with the Google Calendar API (v3) to synchronize booked slots with user Google Calendars.

sequenceDiagram
    autonumber
    actor Admin as Cooperative Admin
    participant App as "Scheduler UI (auth_start.php)"
    participant Google as Google Identity Platform
    participant Callback as "Scheduler API (auth_callback.php)"
    participant DataStore as "JSON Data Store (data/*.json)"
    participant Booking as "Public Customer (api.php)"

    Admin->>App: Click "Connect Google Calendar"
    App->>Google: Redirect to OAuth Consent Screen (access_type=offline, prompt=consent)
    Google->>Admin: Request Calendar Scope Permission
    Admin->>Google: Grant Approval
    Google->>Callback: Redirect with Authorization Code (?code=...)
    Callback->>Google: Exchange Code for Access & Refresh Token
    Google-->>Callback: Return Tokens (access_token, refresh_token, expires_in)
    Callback->>DataStore: Persist Refresh Token into data/{id}.json
    
    Note over Booking,Google: Subsequent Automated Bookings by Clients
    Booking->>DataStore: Load data/{id}.json
    Booking->>Google: fetchAccessTokenWithRefreshToken(refresh_token)
    Google-->>Booking: Fresh Access Token
    Booking->>Google: Insert Event (calendarId=primary)

Offline Access & Refresh Token Guarantees

In scheduler/backend/config_google.php, offline access is strictly configured:

  • setAccessType('offline'): Instructs Google to issue a long-lived Refresh Token.
  • setPrompt('select_account consent'): Forces prompt on consent to ensure Google re-issues the refresh token even if the account was previously authorized.
  • Scopes: Google_Service_Calendar::CALENDAR and email.

4. Anti-Caching & Data Freshness Controls

Because deal configurations, offers, and calendar appointments represent real-time commercial and scheduling data, aggressive anti-caching HTTP response headers are injected into all endpoints:

// Anti-Cache Enforcement across PHP endpoints
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
header("Expires: 0");

This prevents edge caches (Cloudflare, Nginx micro-caches, Hostinger proxy layers, and browser memory caches) from serving stale appointment slots or outdated offer figures.


5. Input Sanitization & Path Traversal Prevention

When loading data records based on user query parameters (such as ?id=XYZ or ?token=XYZ), values are strictly sanitized to block directory traversal (../) and injection attacks:

// Prevent Path Traversal in Scheduler & Dealflow
$cal_id = $_GET['id'] ?? 'default';
$cal_id = preg_replace('/[^a-zA-Z0-9_-]/', '', $cal_id);
$file_path = __DIR__ . "/../data/{$cal_id}.json";
// Token Mode: Only allow lowercase hex characters
$rawToken = $_GET['token'] ?? '';
$cleanToken = preg_replace('/[^a-f0-9]/', '', strtolower($rawToken));
$tokenFile = __DIR__ . '/data/' . $cleanToken . '.json';

Any characters outside alphanumeric and hyphens/underscores are stripped before file system resolution.