Initial commit: Uruk Prize Platform architecture, backend core, mobile app, gateway caller & deployment pipeline
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Middlewares;
|
||||
|
||||
use Core\Middleware;
|
||||
use Core\Request;
|
||||
use Core\Response;
|
||||
use Core\Security;
|
||||
use Core\Database;
|
||||
use PDO;
|
||||
|
||||
class SecurityHeaderMiddleware extends Middleware
|
||||
{
|
||||
public function handle(Request $request, callable $next): void
|
||||
{
|
||||
$userId = $request->getHeader('x-user-id');
|
||||
$fingerprint = $request->getHeader('x-device-fingerprint');
|
||||
$timestamp = (int)$request->getHeader('x-timestamp');
|
||||
$nonce = $request->getHeader('x-nonce');
|
||||
$signature = $request->getHeader('x-signature');
|
||||
|
||||
// 1. Check presence of all required security headers
|
||||
if (empty($userId) || empty($fingerprint) || empty($timestamp) || empty($nonce) || empty($signature)) {
|
||||
Response::unauthorized('Security headers missing: X-User-Id, X-Device-Fingerprint, X-Timestamp, X-Nonce, and X-Signature are mandatory.');
|
||||
}
|
||||
|
||||
// 2. Anti-replay timestamp check (window of 120 seconds)
|
||||
$currentTime = time();
|
||||
if (abs($currentTime - $timestamp) > 120) {
|
||||
Response::unauthorized('Request timestamp expired or skewed. Anti-replay check failed.');
|
||||
}
|
||||
|
||||
// 3. Query user security record from Database
|
||||
try {
|
||||
$pdo = Database::getConnection();
|
||||
$stmt = $pdo->prepare('
|
||||
SELECT us.device_fingerprint, us.device_secret, us.is_locked, u.status
|
||||
FROM user_security us
|
||||
JOIN users u ON u.id = us.user_id
|
||||
WHERE us.user_id = :user_id AND us.device_fingerprint = :fingerprint
|
||||
LIMIT 1
|
||||
');
|
||||
$stmt->execute([
|
||||
':user_id' => $userId,
|
||||
':fingerprint' => $fingerprint,
|
||||
]);
|
||||
$secRecord = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$secRecord) {
|
||||
Response::forbidden('Unrecognized device fingerprint or unauthorized device binding.');
|
||||
}
|
||||
|
||||
if ((int)$secRecord['is_locked'] === 1 || in_array($secRecord['status'], ['SUSPENDED', 'BANNED'], true)) {
|
||||
Response::forbidden('User device or account has been locked or suspended.');
|
||||
}
|
||||
|
||||
// 4. Verify HMAC-SHA256 signature
|
||||
$rawBody = $request->getRawBody();
|
||||
$bodyHash = hash('sha256', $rawBody);
|
||||
$signPayload = implode("\n", [
|
||||
$request->getMethod(),
|
||||
$request->getPath(),
|
||||
(string)$timestamp,
|
||||
$nonce,
|
||||
$bodyHash,
|
||||
]);
|
||||
|
||||
if (!Security::verifyHmacSignature($signPayload, $signature, $secRecord['device_secret'])) {
|
||||
Response::unauthorized('Invalid cryptographic signature for this device and payload.');
|
||||
}
|
||||
|
||||
// 5. Update last active timestamp and IP asynchronously / inline
|
||||
$updateStmt = $pdo->prepare('
|
||||
UPDATE user_security
|
||||
SET last_active_at = NOW(), last_ip = :ip
|
||||
WHERE user_id = :user_id AND device_fingerprint = :fingerprint
|
||||
');
|
||||
$updateStmt->execute([
|
||||
':ip' => $request->getIp(),
|
||||
':user_id' => $userId,
|
||||
':fingerprint' => $fingerprint,
|
||||
]);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
Response::error('Security verification error: ' . $e->getMessage(), 500);
|
||||
}
|
||||
|
||||
$next($request);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user