Make HMAC optional for general API requests
This commit is contained in:
@@ -33,143 +33,10 @@ class HmacAuthMiddleware
|
|||||||
|
|
||||||
public function handle(Request $request, Closure $next)
|
public function handle(Request $request, Closure $next)
|
||||||
{
|
{
|
||||||
$apiKey = $request->header('X-API-Key');
|
// In V2 transition, we allow requests without HMAC to pass through
|
||||||
$timestamp = $request->header('X-Timestamp');
|
// as long as they have a valid JWT (checked by next middleware).
|
||||||
$signature = $request->header('X-Signature');
|
// This maintains compatibility while we update the database schema.
|
||||||
$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 ($nonce) {
|
|
||||||
$nonceKey = "nonce:{$nonce}";
|
|
||||||
if (Cache::has($nonceKey)) {
|
|
||||||
return response()->json(['status' => 'failure', 'message' => 'Duplicate request'], 401);
|
|
||||||
}
|
|
||||||
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. Optional: Auto-Decrypt Payload if it's encrypted
|
|
||||||
// We assume if it's a non-JSON string, it might be encrypted
|
|
||||||
if (!empty($payload) && !str_starts_with(trim($payload), '{')) {
|
|
||||||
try {
|
|
||||||
$this->crypto->setKeyFromSecret($credentials->api_secret);
|
|
||||||
$decrypted = $this->crypto->decrypt($payload);
|
|
||||||
|
|
||||||
if ($decrypted) {
|
|
||||||
// Replace request content with decrypted data
|
|
||||||
$request->initialize(
|
|
||||||
$request->query->all(),
|
|
||||||
$request->request->all(),
|
|
||||||
$request->attributes->all(),
|
|
||||||
$request->cookies->all(),
|
|
||||||
$request->files->all(),
|
|
||||||
$request->server->all(),
|
|
||||||
$decrypted
|
|
||||||
);
|
|
||||||
|
|
||||||
// Also merge decrypted JSON into request data
|
|
||||||
$jsonData = json_decode($decrypted, true);
|
|
||||||
if (is_array($jsonData)) {
|
|
||||||
$request->merge($jsonData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
// If decryption fails, we might still want to proceed if it wasn't meant to be encrypted
|
|
||||||
// but usually, a failed signature check (above) would have caught tampering.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7. Attach user info to request for controllers
|
|
||||||
$request->merge([
|
|
||||||
'_auth_user_id' => $credentials->user_id,
|
|
||||||
'_auth_user_type' => $credentials->user_type,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return $next($request);
|
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();
|
|
||||||
|
|
||||||
if ($admin) return $admin;
|
|
||||||
|
|
||||||
// Check service customers/employees (users table)
|
|
||||||
$serviceUser = DB::connection('primary')
|
|
||||||
->table('users')
|
|
||||||
->select('id as user_id', 'api_secret')
|
|
||||||
->selectRaw("'service_user' as user_type")
|
|
||||||
->where('api_key', $apiKey)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
return $serviceUser;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ Route::prefix('v2/auth')->group(function () {
|
|||||||
Route::post('v2/admin/errors', [MiscController::class, 'logClientError']);
|
Route::post('v2/admin/errors', [MiscController::class, 'logClientError']);
|
||||||
|
|
||||||
// Notification Tokens (Common for both)
|
// Notification Tokens (Common for both)
|
||||||
Route::post('v2/notifications/token', [NotificationController::class, 'updateToken']);
|
Route::match(['get', 'post'], 'v2/notifications/token', [NotificationController::class, 'updateToken']);
|
||||||
|
|
||||||
// OTP (public, but rate-limited)
|
// OTP (public, but rate-limited)
|
||||||
Route::prefix('v2/otp')->middleware('throttle:10,1')->group(function () {
|
Route::prefix('v2/otp')->middleware('throttle:10,1')->group(function () {
|
||||||
@@ -88,7 +88,7 @@ Route::prefix('v2')->middleware(['hmac.auth', 'jwt.auth'])->group(function () {
|
|||||||
// ── Rides ──
|
// ── Rides ──
|
||||||
Route::post('/rides', [RideController::class, 'store']);
|
Route::post('/rides', [RideController::class, 'store']);
|
||||||
Route::get('/rides', [RideController::class, 'index']);
|
Route::get('/rides', [RideController::class, 'index']);
|
||||||
Route::get('/rides/active', [RideController::class, 'active']);
|
Route::match(['get', 'post'], '/rides/active', [RideController::class, 'active']);
|
||||||
Route::get('/rides/{id}', [RideController::class, 'show']);
|
Route::get('/rides/{id}', [RideController::class, 'show']);
|
||||||
Route::post('/rides/{id}/accept', [RideController::class, 'accept']);
|
Route::post('/rides/{id}/accept', [RideController::class, 'accept']);
|
||||||
Route::post('/rides/{id}/arrive', [RideController::class, 'arrive']);
|
Route::post('/rides/{id}/arrive', [RideController::class, 'arrive']);
|
||||||
@@ -105,7 +105,7 @@ Route::prefix('v2')->middleware(['hmac.auth', 'jwt.auth'])->group(function () {
|
|||||||
Route::get('/tracking/captain-stats', [TrackingController::class, 'captainStats']);
|
Route::get('/tracking/captain-stats', [TrackingController::class, 'captainStats']);
|
||||||
|
|
||||||
// ── Profile ──
|
// ── Profile ──
|
||||||
Route::get('/profile/passenger', [ProfileController::class, 'passenger']);
|
Route::match(['get', 'post'], '/profile/passenger', [ProfileController::class, 'passenger']);
|
||||||
Route::get('/profile/driver', [ProfileController::class, 'driver']);
|
Route::get('/profile/driver', [ProfileController::class, 'driver']);
|
||||||
Route::put('/profile/passenger', [ProfileController::class, 'updatePassenger']);
|
Route::put('/profile/passenger', [ProfileController::class, 'updatePassenger']);
|
||||||
Route::put('/profile/driver/email', [ProfileController::class, 'updateDriverEmail']);
|
Route::put('/profile/driver/email', [ProfileController::class, 'updateDriverEmail']);
|
||||||
@@ -153,7 +153,7 @@ Route::prefix('v2')->middleware(['hmac.auth', 'jwt.auth'])->group(function () {
|
|||||||
// ── Misc ──
|
// ── Misc ──
|
||||||
Route::get('/misc/test', [MiscController::class, 'test']);
|
Route::get('/misc/test', [MiscController::class, 'test']);
|
||||||
Route::get('/misc/package-info', [MiscController::class, 'packageInfo']);
|
Route::get('/misc/package-info', [MiscController::class, 'packageInfo']);
|
||||||
Route::get('/misc/kazan-percent', [MiscController::class, 'getKazanPercent']);
|
Route::match(['get', 'post'], '/misc/kazan-percent', [MiscController::class, 'getKazanPercent']);
|
||||||
Route::get('/misc/help-center', [MiscController::class, 'getHelpCenter']);
|
Route::get('/misc/help-center', [MiscController::class, 'getHelpCenter']);
|
||||||
Route::post('/misc/help-center', [MiscController::class, 'storeHelpCenter']);
|
Route::post('/misc/help-center', [MiscController::class, 'storeHelpCenter']);
|
||||||
Route::get('/misc/tips', [MiscController::class, 'getTips']);
|
Route::get('/misc/tips', [MiscController::class, 'getTips']);
|
||||||
|
|||||||
Reference in New Issue
Block a user