196 lines
5.5 KiB
PHP
196 lines
5.5 KiB
PHP
<?php
|
|
/**
|
|
* Authentication & Security Handler
|
|
* HMAC-based request validation for API endpoints
|
|
*/
|
|
|
|
class AuthenticationHandler {
|
|
private $db;
|
|
private const SIGNATURE_ALGORITHM = 'sha256';
|
|
private const TIMESTAMP_TOLERANCE = 300; // 5 minutes in seconds
|
|
|
|
public function __construct() {
|
|
$this->db = Database::getInstance();
|
|
}
|
|
|
|
/**
|
|
* Validate HMAC signature of incoming request
|
|
*
|
|
* @param string $api_key The API key from request header
|
|
* @param string $signature The HMAC signature from request header
|
|
* @param string $payload The raw request body
|
|
* @param string $timestamp The request timestamp
|
|
* @return array ['valid' => bool, 'user_id' => int|null, 'error' => string|null]
|
|
*/
|
|
public function validateHmacSignature($api_key, $signature, $payload, $timestamp) {
|
|
// Validate timestamp to prevent replay attacks
|
|
if (!$this->isValidTimestamp($timestamp)) {
|
|
return [
|
|
'valid' => false,
|
|
'user_id' => null,
|
|
'error' => 'Request timestamp is invalid or expired'
|
|
];
|
|
}
|
|
|
|
// Get user by API key
|
|
$user = $this->getUserByApiKey($api_key);
|
|
if (!$user) {
|
|
// Log suspicious activity
|
|
$this->logSecurityEvent('INVALID_API_KEY', $api_key);
|
|
return [
|
|
'valid' => false,
|
|
'user_id' => null,
|
|
'error' => 'Invalid API key'
|
|
];
|
|
}
|
|
|
|
// Generate expected signature
|
|
$expectedSignature = $this->generateSignature(
|
|
$payload,
|
|
$user['api_secret'],
|
|
$timestamp,
|
|
$api_key
|
|
);
|
|
|
|
// Compare signatures using timing-safe comparison
|
|
if (!hash_equals($expectedSignature, $signature)) {
|
|
// Log failed authentication attempt
|
|
$this->logSecurityEvent('INVALID_SIGNATURE', $api_key, $user['id']);
|
|
return [
|
|
'valid' => false,
|
|
'user_id' => null,
|
|
'error' => 'Invalid signature'
|
|
];
|
|
}
|
|
|
|
// Check if user is active
|
|
if (!$user['is_active']) {
|
|
return [
|
|
'valid' => false,
|
|
'user_id' => null,
|
|
'error' => 'User account is inactive'
|
|
];
|
|
}
|
|
|
|
return [
|
|
'valid' => true,
|
|
'user_id' => $user['id'],
|
|
'error' => null
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Generate HMAC signature
|
|
*
|
|
* Signature format: HMAC-SHA256(timestamp|payload, api_secret)
|
|
*/
|
|
private function generateSignature($payload, $api_secret, $timestamp, $api_key) {
|
|
$data = $timestamp . '|' . $api_key . '|' . $payload;
|
|
return hash_hmac(self::SIGNATURE_ALGORITHM, $data, $api_secret);
|
|
}
|
|
|
|
/**
|
|
* Verify timestamp is within acceptable range
|
|
*/
|
|
private function isValidTimestamp($timestamp) {
|
|
$current_time = time();
|
|
$request_time = (int)$timestamp;
|
|
$time_diff = abs($current_time - $request_time);
|
|
|
|
return $time_diff <= self::TIMESTAMP_TOLERANCE;
|
|
}
|
|
|
|
/**
|
|
* Get user by API key
|
|
*/
|
|
private function getUserByApiKey($api_key) {
|
|
$stmt = $this->db->prepare('
|
|
SELECT id, api_secret, is_active, uuid
|
|
FROM users
|
|
WHERE api_key = ?
|
|
LIMIT 1
|
|
');
|
|
|
|
$stmt->bind_param('s', $api_key);
|
|
$stmt->execute();
|
|
$result = $stmt->get_result();
|
|
|
|
if ($result->num_rows === 0) {
|
|
return null;
|
|
}
|
|
|
|
return $result->fetch_assoc();
|
|
}
|
|
|
|
/**
|
|
* Log security events for audit trail
|
|
*/
|
|
private function logSecurityEvent($event_type, $api_key, $user_id = null) {
|
|
$ip_address = $this->getClientIpAddress();
|
|
$user_agent = $_SERVER['HTTP_USER_AGENT'] ?? 'Unknown';
|
|
|
|
$stmt = $this->db->prepare('
|
|
INSERT INTO api_logs (user_id, endpoint, method, status_code, ip_address, user_agent, error_message, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, NOW())
|
|
');
|
|
|
|
$endpoint = $event_type;
|
|
$method = $_SERVER['REQUEST_METHOD'];
|
|
$status_code = 401;
|
|
$error_msg = $event_type;
|
|
|
|
$stmt->bind_param(
|
|
'issssss',
|
|
$user_id,
|
|
$endpoint,
|
|
$method,
|
|
$status_code,
|
|
$ip_address,
|
|
$user_agent,
|
|
$error_msg
|
|
);
|
|
|
|
$stmt->execute();
|
|
$stmt->close();
|
|
}
|
|
|
|
/**
|
|
* Get client IP address (handles proxies)
|
|
*/
|
|
private function getClientIpAddress() {
|
|
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
|
|
return $_SERVER['HTTP_CLIENT_IP'];
|
|
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
|
$ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
|
|
return trim($ips[0]);
|
|
} else {
|
|
return $_SERVER['REMOTE_ADDR'] ?? 'Unknown';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate API key and secret for new user
|
|
*/
|
|
public static function generateApiCredentials() {
|
|
return [
|
|
'api_key' => bin2hex(random_bytes(32)),
|
|
'api_secret' => bin2hex(random_bytes(32))
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Hash password using bcrypt
|
|
*/
|
|
public static function hashPassword($password) {
|
|
return password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
|
|
}
|
|
|
|
/**
|
|
* Verify password
|
|
*/
|
|
public static function verifyPassword($password, $hash) {
|
|
return password_verify($password, $hash);
|
|
}
|
|
}
|
|
?>
|