The Popup Reward Store API

Security Considerations

B2B security best practices for API key management and safe storage

Security Considerations for B2B Integration

Essential security patterns for B2B clients integrating with the API. This guide focuses on server-to-server authentication security, API key management, and safe credential storage practices.

API Key Management

Credential Rotation Strategy

Implement regular API credential rotation to minimize security risks:

# Rotate API credentials (typically done every 90 days)
# Step 1: Generate new credentials in your dashboard
# Step 2: Test new credentials in staging environment
curl -X POST {{host}}/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "username": "new_api_username",
    "password": "new_secure_password"
  }'

# Step 3: Revoke old tokens by logging out with old credentials
curl -X POST {{host}}/auth/logout \
  -H "Authorization: Bearer $OLD_ACCESS_TOKEN"

# Step 4: Update production systems with new credentials
# Step 5: Verify new credentials work in production
<?php
class APICredentialManager {
    private $host;
    private $rotationSchedule;

    public function __construct($host) {
        $this->host = $host;
        $this->rotationSchedule = 90; // Rotate every 90 days
    }

    public function shouldRotateCredentials($lastRotationDate) {
        $daysSinceRotation = (time() - $lastRotationDate) / (24 * 60 * 60);
        return $daysSinceRotation >= $this->rotationSchedule;
    }

    public function performCredentialRotation($newUsername, $newPassword, $oldAccessToken) {
        try {
            // Step 1: Test new credentials
            $this->testNewCredentials($newUsername, $newPassword);

            // Step 2: Revoke old tokens
            $this->revokeOldTokens($oldAccessToken);

            // Step 3: Update production credentials
            $this->updateProductionCredentials($newUsername, $newPassword);

            // Step 4: Log rotation
            $this->logRotation();

            return true;
        } catch (Exception $e) {
            error_log('Credential rotation failed: ' . $e->getMessage());
            $this->notifySecurityTeam($e);
            return false;
        }
    }

    private function testNewCredentials($username, $password) {
        $data = json_encode([
            'username' => $username,
            'password' => $password
        ]);

        $ch = curl_init($this->host . '/auth/login');
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
        curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($httpCode !== 200) {
            throw new Exception('New credentials authentication failed');
        }

        // Logout the test session
        $result = json_decode($response, true);
        $this->revokeOldTokens($result['data']['access_token']);
    }

    private function revokeOldTokens($accessToken) {
        $ch = curl_init($this->host . '/auth/logout');
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'Authorization: Bearer ' . $accessToken
        ]);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_exec($ch);
        curl_close($ch);
    }

    private function notifySecurityTeam($error) {
        mail(
            'security@yourcompany.com',
            'API Credential Rotation Failed',
            'Error: ' . $error->getMessage() . "\n" .
            'Time: ' . date('Y-m-d H:i:s') . "\n" .
            'System: Production API Integration'
        );
    }
}
?>

Secure Credential Storage

Server-Side Storage Best Practices

Store API credentials securely in your server infrastructure:

# Use environment variables for credentials (recommended)
export API_USERNAME="your_api_username"
export API_PASSWORD="your_secure_password"

# Access credentials from environment
curl -X POST {{host}}/auth/login \
  -H "Content-Type: application/json" \
  -d "{
    \"username\": \"$API_USERNAME\",
    \"password\": \"$API_PASSWORD\"
  }"

# Alternative: Use credential files with restricted permissions
# Create credentials file (readable only by application user)
echo '{"username":"api_user","password":"secure_pass"}' > /etc/app/credentials.json
chmod 600 /etc/app/credentials.json

# Use credentials from file
curl -X POST {{host}}/auth/login \
  -H "Content-Type: application/json" \
  -d @/etc/app/credentials.json

# For containerized environments (Docker/Kubernetes)
# Mount credentials as secrets
docker run -e API_USERNAME="$API_USERNAME" \
           -e API_PASSWORD="$API_PASSWORD" \
           your-app:latest
<?php
class SecureCredentialStorage {
    private const ENCRYPTION_METHOD = 'AES-256-GCM';
    private $encryptionKey;

    public function __construct() {
        $this->encryptionKey = $this->loadEncryptionKey();
    }

    // Method 1: Environment Variables (Recommended)
    public function loadFromEnvironment() {
        return [
            'username' => getenv('API_USERNAME'),
            'password' => getenv('API_PASSWORD')
        ];
    }

    // Method 2: Encrypted Configuration Files
    public function loadFromEncryptedFile($filePath) {
        if (!file_exists($filePath)) {
            throw new Exception('Credentials file not found');
        }

        // Verify file permissions (should be 600)
        $permissions = fileperms($filePath) & 0777;
        if ($permissions !== 0600) {
            throw new Exception('Credentials file has insecure permissions');
        }

        $encryptedData = file_get_contents($filePath);
        return $this->decrypt($encryptedData);
    }

    // Method 3: Database Storage (with encryption)
    public function loadFromDatabase($pdo, $credentialId) {
        $stmt = $pdo->prepare(
            "SELECT encrypted_credentials FROM api_credentials WHERE id = ? AND active = 1"
        );
        $stmt->execute([$credentialId]);

        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$row) {
            throw new Exception('Credentials not found');
        }

        return $this->decrypt($row['encrypted_credentials']);
    }

    // Method 4: External Secret Management (HashiCorp Vault, AWS Secrets Manager)
    public function loadFromVault($secretPath) {
        $vaultToken = getenv('VAULT_TOKEN');
        $vaultUrl = getenv('VAULT_ADDR') . '/v1/' . $secretPath;

        $context = stream_context_create([
            'http' => [
                'method' => 'GET',
                'header' => 'X-Vault-Token: ' . $vaultToken
            ]
        ]);

        $response = file_get_contents($vaultUrl, false, $context);
        $data = json_decode($response, true);

        return [
            'username' => $data['data']['username'],
            'password' => $data['data']['password']
        ];
    }

    private function encrypt($data) {
        $iv = random_bytes(16);
        $tag = '';
        $encrypted = openssl_encrypt(
            json_encode($data),
            self::ENCRYPTION_METHOD,
            $this->encryptionKey,
            OPENSSL_RAW_DATA,
            $iv,
            $tag
        );

        return base64_encode($iv . $tag . $encrypted);
    }

    private function decrypt($encryptedData) {
        $data = base64_decode($encryptedData);
        $iv = substr($data, 0, 16);
        $tag = substr($data, 16, 16);
        $encrypted = substr($data, 32);

        $decrypted = openssl_decrypt(
            $encrypted,
            self::ENCRYPTION_METHOD,
            $this->encryptionKey,
            OPENSSL_RAW_DATA,
            $iv,
            $tag
        );

        return json_decode($decrypted, true);
    }

    private function loadEncryptionKey() {
        $key = getenv('ENCRYPTION_KEY');
        if (!$key) {
            throw new Exception('Encryption key not found');
        }
        return base64_decode($key);
    }
}
?>

Token Storage and Management

Access Token Security

Handle access tokens securely in your server applications:

# Store tokens in memory or secure cache (Redis with encryption)
# Never store tokens in log files or databases without encryption

# Use secure headers when making authenticated requests
curl -X GET {{host}}/api/v1/products \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "User-Agent: YourCompany-Integration/1.0" \
  -H "X-Request-ID: $(uuidgen)" \
  --connect-timeout 30 \
  --max-time 60 \
  --retry 3 \
  --retry-delay 1
<?php
class SecureTokenManager {
    private $redis;
    private $encryptionKey;

    public function __construct() {
        $this->redis = new Redis();
        $this->redis->connect('127.0.0.1', 6379);
        $this->encryptionKey = $this->loadEncryptionKey();
    }

    public function storeTokens($tokenData) {
        $data = [
            'access_token' => $tokenData['access_token'],
            'refresh_token' => $tokenData['refresh_token'],
            'expires_at' => strtotime($tokenData['access_expires_at']),
            'created_at' => time()
        ];

        // Encrypt before storing
        $encryptedData = $this->encrypt($data);

        // Store in Redis with TTL matching refresh token lifetime (7 days + buffer)
        $key = 'api_tokens:' . $this->getClientIdentifier();
        $this->redis->setex($key, 7 * 24 * 60 * 60 + 300, $encryptedData);

        // Log token creation (without token values)
        error_log('New tokens stored for client: ' . $this->getClientIdentifier());
    }

    public function getValidAccessToken() {
        $key = 'api_tokens:' . $this->getClientIdentifier();
        $encryptedData = $this->redis->get($key);

        if (!$encryptedData) {
            throw new Exception('No tokens found');
        }

        $tokenData = $this->decrypt($encryptedData);

        // Check if token is near expiration (refresh 5 minutes early)
        if ((time() + 300) >= $tokenData['expires_at']) {
            $this->refreshTokens($tokenData['refresh_token']);
            return $this->getValidAccessToken();
        }

        return $tokenData['access_token'];
    }

    public function clearTokens() {
        $key = 'api_tokens:' . $this->getClientIdentifier();
        $this->redis->del($key);
        error_log('Tokens cleared for client: ' . $this->getClientIdentifier());
    }

    public function makeSecureRequest($url, $method = 'GET', $data = null) {
        $requestId = uniqid('req_', true);
        $startTime = microtime(true);

        try {
            $accessToken = $this->getValidAccessToken();

            $headers = [
                'Authorization: Bearer ' . $accessToken,
                'Content-Type: application/json',
                'User-Agent: YourCompany-Integration/1.0',
                'X-Request-ID: ' . $requestId
            ];

            $ch = curl_init($url);
            curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
            curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($ch, CURLOPT_TIMEOUT, 30);

            if ($data) {
                curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
            }

            $response = curl_exec($ch);
            $duration = microtime(true) - $startTime;

            // Log successful request (without sensitive data)
            error_log(sprintf(
                'API Request: %s %s - Duration: %.3fs - Request ID: %s',
                $method,
                parse_url($url, PHP_URL_PATH),
                $duration,
                $requestId
            ));

            curl_close($ch);
            return json_decode($response, true);

        } catch (Exception $e) {
            $duration = microtime(true) - $startTime;

            error_log(sprintf(
                'API Request Failed: %s %s - Error: %s - Duration: %.3fs - Request ID: %s',
                $method,
                parse_url($url, PHP_URL_PATH),
                $e->getMessage(),
                $duration,
                $requestId
            ));

            throw $e;
        }
    }
}
?>

Network Security

HTTPS and Certificate Validation

Ensure secure communication with the API:

# Always use HTTPS and verify certificates
curl -X GET {{host}}/api/v1/products \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  --cacert /path/to/ca-certificates.crt \
  --tlsv1.2 \
  --ssl-reqd

# NEVER use --insecure or -k flags in production
# These disable certificate verification
<?php
class SecureHTTPClient {
    public function makeRequest($url, $method = 'GET', $data = null, $headers = []) {
        // Validate URL is HTTPS
        if (strpos($url, 'https://') !== 0) {
            throw new Exception('Only HTTPS URLs are allowed');
        }

        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_TIMEOUT, 30);

        // SSL/TLS security settings
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
        curl_setopt($ch, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2);

        if (!empty($headers)) {
            curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        }

        if ($data && in_array($method, ['POST', 'PUT', 'PATCH'])) {
            curl_setopt($ch, CURLOPT_POSTFIELDS,
                is_string($data) ? $data : json_encode($data)
            );
        }

        $response = curl_exec($ch);

        if ($response === false) {
            $error = curl_error($ch);
            curl_close($ch);
            throw new Exception('HTTP request failed: ' . $error);
        }

        curl_close($ch);
        return $response;
    }
}
?>

Monitoring and Alerting

Security Event Monitoring

Monitor your integration for security issues:

# Log all API requests and monitor for anomalies
# Track authentication failures (401) and access denials (403)

#!/bin/bash
# monitor-api-security.sh

LOG_FILE="/var/log/api-integration.log"
ALERT_EMAIL="security@yourcompany.com"

# Count suspicious events in the last hour
FAILED_AUTHS=$(grep -c "401" $LOG_FILE 2>/dev/null || echo 0)
FORBIDDEN_ACCESS=$(grep -c "403" $LOG_FILE 2>/dev/null || echo 0)

if [ "$FAILED_AUTHS" -gt 10 ]; then
    echo "High number of failed authentications: $FAILED_AUTHS" | \
    mail -s "API Security Alert: Auth Failures" $ALERT_EMAIL
fi

if [ "$FORBIDDEN_ACCESS" -gt 5 ]; then
    echo "High number of forbidden access attempts: $FORBIDDEN_ACCESS" | \
    mail -s "API Security Alert: IP Blocks" $ALERT_EMAIL
fi

# Run this script every 15 minutes via cron
# */15 * * * * /path/to/monitor-api-security.sh
<?php
class SecurityMonitor {
    private $alertThresholds = [
        'failed_auth' => 10,
        'forbidden_access' => 5,
    ];

    public function monitorSecurityEvents() {
        $events = $this->collectSecurityEvents();

        foreach ($events as $eventType => $count) {
            if (isset($this->alertThresholds[$eventType]) &&
                $count > $this->alertThresholds[$eventType]) {
                $this->sendSecurityAlert($eventType, $count);
            }
        }
    }

    private function collectSecurityEvents() {
        $events = [
            'failed_auth' => 0,
            'forbidden_access' => 0,
        ];

        $logFile = '/var/log/api-integration.log';
        if (file_exists($logFile)) {
            $logs = file_get_contents($logFile);
            $events['failed_auth'] = substr_count($logs, '401');
            $events['forbidden_access'] = substr_count($logs, '403');
        }

        return $events;
    }

    private function sendSecurityAlert($eventType, $count) {
        $subject = 'API Security Alert: ' . ucwords(str_replace('_', ' ', $eventType));
        $message = sprintf(
            "Security event detected:\n" .
            "Event Type: %s\n" .
            "Count: %d\n" .
            "Threshold: %d\n" .
            "Time: %s\n" .
            "Server: %s\n",
            $eventType,
            $count,
            $this->alertThresholds[$eventType],
            date('Y-m-d H:i:s'),
            gethostname()
        );

        mail('security@yourcompany.com', $subject, $message);
        error_log("Security alert sent: $eventType ($count events)");
    }
}

// Set up monitoring cron job
if (php_sapi_name() === 'cli') {
    $monitor = new SecurityMonitor();
    $monitor->monitorSecurityEvents();
}
?>

B2B Security Best Practices Summary

1. Credential Management

  • Rotate API credentials every 90 days minimum
  • Use environment variables or secure vaults for credential storage
  • Never commit credentials to version control
  • Test new credentials in staging before updating production

2. Token Security

  • Store access tokens in encrypted, secure storage (Redis with encryption, database)
  • Implement automatic token refresh before expiration
  • Clear tokens immediately when logging out
  • Monitor token usage patterns for anomalies

3. Network Security

  • Always use HTTPS for API communications
  • Validate SSL certificates — never disable verification
  • Use TLS 1.2 or higher
  • Implement proper timeout and retry logic

4. IP Whitelisting

  • Configure IP whitelist entries for your production servers
  • Update whitelist when server IPs change
  • Monitor 403 responses that may indicate IP configuration issues

5. Monitoring and Alerting

  • Log all authentication attempts and failures
  • Set up automated alerts for suspicious activity (excessive 401s, 403s)
  • Monitor API usage patterns and error rates
  • Implement security incident response procedures