Initial V2 commit
This commit is contained in:
136
app/Http/Middleware/HmacAuthMiddleware.php
Normal file
136
app/Http/Middleware/HmacAuthMiddleware.php
Normal file
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
/**
|
||||
* HMAC Signature Validation Middleware
|
||||
*
|
||||
* Validates every API request using HMAC-SHA256 signatures.
|
||||
* Prevents: Replay attacks, Man-in-the-Middle, Tampering.
|
||||
*
|
||||
* Required Headers:
|
||||
* X-API-Key: unique per user (stored in DB)
|
||||
* X-Timestamp: Unix timestamp (must be within TOLERANCE window)
|
||||
* X-Signature: HMAC-SHA256(timestamp|api_key|request_body, api_secret)
|
||||
* X-Nonce: unique per request (prevents replay attacks)
|
||||
*/
|
||||
class HmacAuthMiddleware
|
||||
{
|
||||
private const ALGORITHM = 'sha256';
|
||||
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
$apiKey = $request->header('X-API-Key');
|
||||
$timestamp = $request->header('X-Timestamp');
|
||||
$signature = $request->header('X-Signature');
|
||||
$nonce = $request->header('X-Nonce');
|
||||
|
||||
// 1. Check required headers
|
||||
if (!$apiKey || !$timestamp || !$signature) {
|
||||
return response()->json([
|
||||
'status' => 'failure',
|
||||
'message' => 'Missing authentication headers'
|
||||
], 401);
|
||||
}
|
||||
|
||||
// 2. Validate timestamp (prevent replay attacks)
|
||||
$tolerance = (int) config('intaleq.hmac_tolerance', 300);
|
||||
$timeDiff = abs(time() - (int) $timestamp);
|
||||
|
||||
if ($timeDiff > $tolerance) {
|
||||
return response()->json([
|
||||
'status' => 'failure',
|
||||
'message' => 'Request expired'
|
||||
], 401);
|
||||
}
|
||||
|
||||
// 3. Check nonce uniqueness (if provided)
|
||||
if ($nonce) {
|
||||
$nonceKey = "nonce:{$nonce}";
|
||||
if (Cache::has($nonceKey)) {
|
||||
return response()->json([
|
||||
'status' => 'failure',
|
||||
'message' => 'Duplicate request'
|
||||
], 401);
|
||||
}
|
||||
// Store nonce for double the tolerance window
|
||||
Cache::put($nonceKey, true, $tolerance * 2);
|
||||
}
|
||||
|
||||
// 4. Lookup API secret from database
|
||||
$credentials = $this->getApiCredentials($apiKey);
|
||||
|
||||
if (!$credentials) {
|
||||
return response()->json([
|
||||
'status' => 'failure',
|
||||
'message' => 'Invalid API key'
|
||||
], 401);
|
||||
}
|
||||
|
||||
// 5. Reconstruct and verify HMAC signature
|
||||
$payload = $request->getContent();
|
||||
$message = "{$timestamp}|{$apiKey}|{$payload}";
|
||||
$expectedSignature = hash_hmac(self::ALGORITHM, $message, $credentials->api_secret);
|
||||
|
||||
if (!hash_equals($expectedSignature, $signature)) {
|
||||
return response()->json([
|
||||
'status' => 'failure',
|
||||
'message' => 'Invalid signature'
|
||||
], 401);
|
||||
}
|
||||
|
||||
// 6. Attach user info to request for controllers
|
||||
$request->merge([
|
||||
'_auth_user_id' => $credentials->user_id,
|
||||
'_auth_user_type' => $credentials->user_type,
|
||||
]);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API credentials with Redis caching (5 min)
|
||||
*/
|
||||
private function getApiCredentials(string $apiKey): ?object
|
||||
{
|
||||
$cacheKey = "api_cred:{$apiKey}";
|
||||
|
||||
return Cache::remember($cacheKey, 300, function () use ($apiKey) {
|
||||
// Check both driver and passenger tables for API keys
|
||||
$driver = DB::connection('primary')
|
||||
->table('driver')
|
||||
->select('id as user_id', 'api_secret')
|
||||
->selectRaw("'driver' as user_type")
|
||||
->where('api_key', $apiKey)
|
||||
->where('status', 'notDeleted')
|
||||
->first();
|
||||
|
||||
if ($driver) return $driver;
|
||||
|
||||
$passenger = DB::connection('primary')
|
||||
->table('passengers')
|
||||
->select('id as user_id', 'api_secret')
|
||||
->selectRaw("'passenger' as user_type")
|
||||
->where('api_key', $apiKey)
|
||||
->where('status', 'notDeleted')
|
||||
->first();
|
||||
|
||||
if ($passenger) return $passenger;
|
||||
|
||||
// Check admin users
|
||||
$admin = DB::connection('primary')
|
||||
->table('adminUser')
|
||||
->select('id as user_id', 'api_secret')
|
||||
->selectRaw("'admin' as user_type")
|
||||
->where('api_key', $apiKey)
|
||||
->first();
|
||||
|
||||
return $admin;
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user