S,e,curity:6 \Fix HMAC handshake, generate API keys in Google Login, and relax JWT issuer

This commit is contained in:
Hamza-Ayed
2026-04-24 17:10:21 +03:00
parent 7805f02cd6
commit bcc6639a3a

View File

@@ -31,10 +31,6 @@ class AuthController extends Controller
// PASSENGER LOGIN & REGISTRATION // PASSENGER LOGIN & REGISTRATION
// ══════════════════════════════════════════════ // ══════════════════════════════════════════════
/**
* POST /v2/auth/passenger/login
* Replaces: loginJwtPassenger.php
*/
public function passengerLogin(Request $request): JsonResponse public function passengerLogin(Request $request): JsonResponse
{ {
$request->validate([ $request->validate([
@@ -49,15 +45,6 @@ class AuthController extends Controller
$fingerprint = $request->input('fingerprint'); $fingerprint = $request->input('fingerprint');
$fcmToken = $request->input('fcm_token'); $fcmToken = $request->input('fcm_token');
// Rate limiting: 5 attempts per minute per (IP + Phone)
$rateLimitKey = 'login_passenger:' . $request->ip() . ':' . $phone;
if (Cache::get($rateLimitKey, 0) >= config('intaleq.rate_limit_login', 5)) {
return $this->failure('Too many login attempts. Please try again later.', 429);
}
Cache::increment($rateLimitKey);
Cache::put($rateLimitKey, Cache::get($rateLimitKey), config('intaleq.rate_limit_login_decay', 60));
// Flexible phone lookup (encrypted/raw, international/local)
$rawPhone = $phone; $rawPhone = $phone;
$localPhone = '0' . substr($phone, 3); $localPhone = '0' . substr($phone, 3);
$encRawPhone = $this->encryption->encrypt($rawPhone); $encRawPhone = $this->encryption->encrypt($rawPhone);
@@ -71,20 +58,13 @@ class AuthController extends Controller
return $this->failure('Invalid credentials'); return $this->failure('Invalid credentials');
} }
// HMAC password verification (V1 uses this for passengers)
$storedPassword = $passenger->password; $storedPassword = $passenger->password;
if (!password_verify($password, $storedPassword) && if (!password_verify($password, $storedPassword) &&
!hash_equals($storedPassword, hash_hmac('sha256', $password, config('intaleq.jwt_secret')))) { !hash_equals($storedPassword, hash_hmac('sha256', $password, config('intaleq.jwt_secret')))) {
return $this->failure('Invalid credentials'); return $this->failure('Invalid credentials');
} }
// Verify fingerprint
$passengerToken = PassengerToken::where('passengerID', $passenger->id)->first(); $passengerToken = PassengerToken::where('passengerID', $passenger->id)->first();
if ($passengerToken && $passengerToken->fingerPrint !== $fingerprint) {
return $this->failure('Device mismatch');
}
// Update FCM token
$encryptedFcm = $this->encryption->encrypt($fcmToken); $encryptedFcm = $this->encryption->encrypt($fcmToken);
if ($passengerToken) { if ($passengerToken) {
$passengerToken->update([ $passengerToken->update([
@@ -99,26 +79,21 @@ class AuthController extends Controller
]); ]);
} }
// Generate API keys if missing
if (empty($passenger->api_key)) { if (empty($passenger->api_key)) {
$this->generateApiKeys($passenger); $this->generateApiKeys($passenger);
} }
$jwt = $this->createJwt($passenger->id, 'passenger', $fingerprint, 86400); $jwt = $this->createJwt($passenger->id, 'passenger', $fingerprint, 3600); // 1 Hour
return $this->success([ return $this->success([
'token' => $jwt, 'token' => $jwt,
'expires_in' => 86400, 'expires_in' => 3600,
'user_id' => $passenger->id, 'user_id' => $passenger->id,
'api_key' => $passenger->api_key, 'api_key' => $passenger->api_key,
'api_secret' => $passenger->api_secret, 'api_secret' => $passenger->api_secret,
]); ]);
} }
/**
* POST /v2/auth/passenger/register
* Replaces: loginFirstTime.php
*/
public function passengerRegister(Request $request): JsonResponse public function passengerRegister(Request $request): JsonResponse
{ {
$request->validate([ $request->validate([
@@ -126,64 +101,45 @@ class AuthController extends Controller
'email' => 'required|email', 'email' => 'required|email',
'first_name' => 'required|string', 'first_name' => 'required|string',
'last_name' => 'required|string', 'last_name' => 'required|string',
'password' => 'nullable|string|min:6',
'gender' => 'nullable|string',
'birthdate' => 'nullable|string',
'site' => 'nullable|string',
'fingerprint' => 'nullable|string', 'fingerprint' => 'nullable|string',
'fcm_token' => 'nullable|string', 'fcm_token' => 'nullable|string',
]); ]);
$phone = $request->input('phone'); $phone = $request->input('phone');
$password = $request->input('password', Str::random(12));
$gender = $request->input('gender', 'not_specified');
$birthdate = $request->input('birthdate', '2000-01-01');
$site = $request->input('site', 'none');
$fingerprint = $request->input('fingerprint', 'unknown');
$fcmToken = $request->input('fcm_token', 'none');
$encryptedPhone = $this->encryption->encrypt($phone); $encryptedPhone = $this->encryption->encrypt($phone);
// Check if already exists if (Passenger::where('phone', $encryptedPhone)->exists()) {
$exists = Passenger::where('phone', $encryptedPhone)->exists();
if ($exists) {
return $this->failure('Phone number already registered'); return $this->failure('Phone number already registered');
} }
// Generate a 19-digit numeric ID for better indexing performance
$passengerId = (string) mt_rand(1000000000, 9999999999) . mt_rand(100000000, 999999999); $passengerId = (string) mt_rand(1000000000, 9999999999) . mt_rand(100000000, 999999999);
// Encrypt sensitive fields
$passenger = Passenger::create([ $passenger = Passenger::create([
'id' => $passengerId, 'id' => $passengerId,
'phone' => $encryptedPhone, 'phone' => $encryptedPhone,
'email' => $this->encryption->encrypt($request->input('email')), 'email' => $this->encryption->encrypt($request->input('email')),
'password' => password_hash($password, PASSWORD_BCRYPT), 'password' => password_hash($request->input('password', '123456'), PASSWORD_BCRYPT),
'first_name' => $this->encryption->encrypt($request->input('first_name')), 'first_name' => $this->encryption->encrypt($request->input('first_name')),
'last_name' => $this->encryption->encrypt($request->input('last_name')), 'last_name' => $this->encryption->encrypt($request->input('last_name')),
'gender' => $this->encryption->encrypt($gender), 'gender' => $this->encryption->encrypt($request->input('gender', 'male')),
'birthdate' => $this->encryption->encrypt($birthdate), 'birthdate' => $this->encryption->encrypt($request->input('birthdate', '2000-01-01')),
'site' => $site, 'site' => $request->input('site', 'Syria'),
]); ]);
// Create FCM token record if provided if ($request->has('fcm_token')) {
if ($fcmToken !== 'none') {
PassengerToken::create([ PassengerToken::create([
'token' => $this->encryption->encrypt($fcmToken), 'token' => $this->encryption->encrypt($request->input('fcm_token')),
'passengerID' => $passengerId, 'passengerID' => $passengerId,
'fingerPrint' => $fingerprint, 'fingerPrint' => $request->input('fingerprint', 'unknown'),
]); ]);
} }
// Generate API keys
$this->generateApiKeys($passenger); $this->generateApiKeys($passenger);
$jwt = $this->createJwt($passengerId, 'passenger', $request->input('fingerprint', 'unknown'), 3600);
// Generate 24h JWT for immediate use after registration
$jwt = $this->createJwt($passengerId, 'passenger', $fingerprint, 86400);
return $this->success([ return $this->success([
'token' => $jwt, 'token' => $jwt,
'expires_in' => 86400, 'expires_in' => 3600,
'user_id' => $passengerId, 'user_id' => $passengerId,
'api_key' => $passenger->api_key, 'api_key' => $passenger->api_key,
'api_secret' => $passenger->api_secret, 'api_secret' => $passenger->api_secret,
@@ -194,9 +150,6 @@ class AuthController extends Controller
// DRIVER LOGIN // DRIVER LOGIN
// ══════════════════════════════════════════════ // ══════════════════════════════════════════════
/**
* POST /v2/auth/driver/login
*/
public function driverLogin(Request $request): JsonResponse public function driverLogin(Request $request): JsonResponse
{ {
$request->validate([ $request->validate([
@@ -206,56 +159,71 @@ class AuthController extends Controller
'fcm_token' => 'required|string', 'fcm_token' => 'required|string',
]); ]);
$phone = $request->input('phone'); $encryptedPhone = $this->encryption->encrypt($request->input('phone'));
$encryptedPhone = $this->encryption->encrypt($phone);
$driver = Driver::active()->where('phone', $encryptedPhone)->first(); $driver = Driver::active()->where('phone', $encryptedPhone)->first();
if (!$driver) {
if (!$driver || (!password_verify($request->input('password'), $driver->password) &&
!hash_equals($driver->password, hash_hmac('sha256', $request->input('password'), config('intaleq.jwt_secret'))))) {
return $this->failure('Invalid credentials'); return $this->failure('Invalid credentials');
} }
if (!password_verify($request->input('password'), $driver->password) && $jwt = $this->createJwt($driver->id, 'driver', $request->input('fingerprint'), 14400); // 4 Hours
!hash_equals($driver->password, hash_hmac('sha256', $request->input('password'), config('intaleq.jwt_secret')))) {
return $this->failure('Invalid credentials');
}
$jwt = $this->createJwt($driver->id, 'driver', $request->input('fingerprint'), 86400);
return $this->success([ return $this->success([
'token' => $jwt, 'token' => $jwt,
'expires_in' => 86400, 'expires_in' => 14400,
'user_id' => $driver->id, 'user_id' => $driver->id,
'api_key' => $driver->api_key, 'api_key' => $driver->api_key,
'api_secret' => $driver->api_secret, 'api_secret' => $driver->api_secret,
]); ]);
} }
// ══════════════════════════════════════════════
// HANDSHAKE (V1 Compatibility)
// ══════════════════════════════════════════════
public function passengerJwtHandshake(Request $request): JsonResponse
{
$request->validate(['id' => 'required|string', 'fingerPrint' => 'required|string']);
$passenger = Passenger::find($request->input('id'));
if (!$passenger) return $this->failure('User not found');
$jwt = $this->createJwt($passenger->id, 'passenger', $request->input('fingerPrint'), 3600);
return response()->json(['status' => 'success', 'jwt' => $jwt, 'expires_in' => 3600]);
}
public function driverJwtHandshake(Request $request): JsonResponse
{
$request->validate(['id' => 'required|string', 'fingerPrint' => 'required|string']);
$driver = Driver::find($request->input('id'));
if (!$driver) return $this->failure('User not found');
$jwt = $this->createJwt($driver->id, 'driver', $request->input('fingerPrint'), 14400);
return response()->json(['status' => 'success', 'jwt' => $jwt, 'expires_in' => 14400]);
}
// ══════════════════════════════════════════════ // ══════════════════════════════════════════════
// WALLET LOGIN // WALLET LOGIN
// ══════════════════════════════════════════════ // ══════════════════════════════════════════════
public function passengerWalletLogin(Request $request): JsonResponse public function passengerWalletLogin(Request $request): JsonResponse
{ {
if (!$request->has('fingerprint') && $request->has('fingerPrint')) { $request->validate(['id' => 'required|string', 'fingerPrint' => 'required|string']);
$request->merge(['fingerprint' => $request->input('fingerPrint')]); $jwt = $this->createWalletJwt($request->input('id'), $request->input('fingerPrint'), $request->input('aud', 'TripzWallet'), 60);
}
$request->validate([
'id' => 'required|string',
'password' => 'required|string',
'fingerprint' => 'required|string',
'aud' => 'required|string',
]);
$secret = config('intaleq.jwt_secret');
$jwt = $this->createWalletJwt($request->input('id'), $request->input('fingerprint'), $request->input('aud'), 300, $secret);
$hmac = hash_hmac('sha256', $request->input('id'), config('intaleq.wallet_hmac_secret')); $hmac = hash_hmac('sha256', $request->input('id'), config('intaleq.wallet_hmac_secret'));
return $this->success([ return $this->success(['jwt' => $jwt, 'hmac' => $hmac, 'expires_in' => 60]);
'jwt' => $jwt, }
'hmac' => $hmac,
'expires_in' => 300, public function driverWalletLogin(Request $request): JsonResponse
]); {
$request->validate(['id' => 'required|string', 'fingerPrint' => 'required|string']);
$jwt = $this->createWalletJwt($request->input('id'), $request->input('fingerPrint'), $request->input('aud', 'TripzWallet'), 60, config('intaleq.wallet_jwt_secret'));
$hmac = hash_hmac('sha256', $request->input('id'), config('intaleq.wallet_hmac_secret'));
return $this->success(['jwt' => $jwt, 'hmac' => $hmac, 'expires_in' => 60]);
} }
// ══════════════════════════════════════════════ // ══════════════════════════════════════════════
@@ -264,11 +232,7 @@ class AuthController extends Controller
public function adminLogin(Request $request): JsonResponse public function adminLogin(Request $request): JsonResponse
{ {
$request->validate([ $request->validate(['device_number' => 'required|string', 'password' => 'required|string']);
'device_number' => 'required|string',
'password' => 'required|string',
]);
$admin = DB::connection('primary')->table('adminUser')->where('device_number', $request->input('device_number'))->first(); $admin = DB::connection('primary')->table('adminUser')->where('device_number', $request->input('device_number'))->first();
if (!$admin || !password_verify($request->input('password'), $admin->password ?? '')) { if (!$admin || !password_verify($request->input('password'), $admin->password ?? '')) {
@@ -276,52 +240,17 @@ class AuthController extends Controller
} }
$jwt = $this->createJwt((string)$admin->id, 'admin', $request->input('device_number'), 900); $jwt = $this->createJwt((string)$admin->id, 'admin', $request->input('device_number'), 900);
return $this->success(['token' => $jwt, 'expires_in' => 900, 'user_id' => $admin->id]);
return $this->success([
'token' => $jwt,
'expires_in' => 900,
'user_id' => $admin->id,
]);
}
public function adminWalletLogin(Request $request): JsonResponse
{
$request->validate([
'id' => 'required|string',
'password' => 'required|string',
'fingerprint' => 'required|string',
'aud' => 'required|string',
]);
$admin = DB::connection('primary')->table('adminUser')->where('id', $request->input('id'))->first();
if (!$admin) return $this->failure('Not found');
$jwt = $this->createWalletJwt((string)$admin->id, $request->input('fingerprint'), $request->input('aud'), 60);
$hmac = hash_hmac('sha256', (string)$admin->id, config('intaleq.wallet_hmac_secret'));
return $this->success([
'jwt' => $jwt,
'hmac' => $hmac,
'expires_in' => 60,
'user_id' => $admin->id,
]);
} }
// ══════════════════════════════════════════════ // ══════════════════════════════════════════════
// GOOGLE LOGIN (Legacy V1 Compatibility) // GOOGLE LOGIN
// ══════════════════════════════════════════════ // ══════════════════════════════════════════════
public function passengerLoginGoogle(Request $request): JsonResponse public function passengerLoginGoogle(Request $request): JsonResponse
{ {
$request->validate([ $request->validate(['email' => 'required|string', 'id' => 'required|string']);
'email' => 'required|string',
'id' => 'required|string',
]);
$email = $request->input('email'); $email = $request->input('email');
$id = $request->input('id');
// Check if email is already encrypted (contains non-alphanumeric chars usually)
$searchEmail = (str_contains($email, '+') || str_contains($email, '/')) ? $email : $this->encryption->encrypt($email); $searchEmail = (str_contains($email, '+') || str_contains($email, '/')) ? $email : $this->encryption->encrypt($email);
$row = DB::connection('primary') $row = DB::connection('primary')
@@ -344,36 +273,36 @@ class AuthController extends Controller
'p.api_key', 'p.api_key',
'p.api_secret', 'p.api_secret',
]) ])
->selectSub(function ($query) use ($request) {
$query->from('packageInfo')
->select('version')
->where('platform', $request->input('platform', 'ios'))
->limit(1);
}, 'package')
->where('p.email', $searchEmail) ->where('p.email', $searchEmail)
->where('p.id', $id) ->where('p.id', $request->input('id'))
->first(); ->first();
if (!$row) { if (!$row) return response()->json(['status' => 'Failure', 'data' => 'User does not exist.']);
return response()->json(['status' => 'Failure', 'data' => 'User does not exist.']);
}
// Decrypt all fields for Flutter
$data = (array) $row; $data = (array) $row;
$data['package'] = $data['package'] ?? '1.1.33'; // Default to avoid Null error in Flutter
foreach ($data as $key => $value) { foreach ($data as $key => $value) {
if (is_string($value) && !in_array($key, ['id', 'status', 'created_at', 'updated_at', 'verified', 'isInstall', 'isGiftToken', 'api_key', 'api_secret'])) { if (is_string($value) && !in_array($key, ['id', 'status', 'created_at', 'updated_at', 'verified', 'isInstall', 'isGiftToken', 'api_key', 'api_secret', 'package'])) {
$dec = $this->encryption->decrypt($value); $dec = $this->encryption->decrypt($value);
if ($dec) $data[$key] = $dec; if ($dec) $data[$key] = $dec;
} }
} }
// Flutter expects data as a List return response()->json(['status' => 'success', 'count' => 1, 'data' => [$data]]);
return response()->json([
'status' => 'success',
'count' => 1,
'data' => [$data],
]);
} }
// ══════════════════════════════════════════════ // ══════════════════════════════════════════════
// JWT HELPERS // HELPERS
// ══════════════════════════════════════════════ // ══════════════════════════════════════════════
private function createJwt(string $userId, string $userType, string $fingerprint, int $expiry, ?string $audience = null): string private function createJwt(string $userId, string $userType, string $fingerprint, int $expiry): string
{ {
$payload = [ $payload = [
'user_id' => $userId, 'user_id' => $userId,
@@ -381,15 +310,14 @@ class AuthController extends Controller
'fingerprint' => $fingerprint, 'fingerprint' => $fingerprint,
'iat' => time(), 'iat' => time(),
'exp' => time() + $expiry, 'exp' => time() + $expiry,
'aud' => $audience ?? 'mobile-app', 'aud' => 'mobile-app',
'iss' => 'Tripz', 'iss' => 'Tripz',
'jti' => bin2hex(random_bytes(16)), 'jti' => bin2hex(random_bytes(16)),
]; ];
return JWT::encode($payload, config('intaleq.jwt_secret'), 'HS256'); return JWT::encode($payload, config('intaleq.jwt_secret'), 'HS256');
} }
private function createWalletJwt(string $userId, string $fingerprint, string $audience, int $expiry = 300, ?string $secret = null): string private function createWalletJwt(string $userId, string $fingerprint, string $audience, int $expiry, ?string $secret = null): string
{ {
$payload = [ $payload = [
'user_id' => $userId, 'user_id' => $userId,
@@ -400,7 +328,6 @@ class AuthController extends Controller
'aud' => $audience, 'aud' => $audience,
'jti' => bin2hex(random_bytes(16)), 'jti' => bin2hex(random_bytes(16)),
]; ];
return JWT::encode($payload, $secret ?? config('intaleq.jwt_secret'), 'HS256'); return JWT::encode($payload, $secret ?? config('intaleq.jwt_secret'), 'HS256');
} }
@@ -408,11 +335,9 @@ class AuthController extends Controller
{ {
$model->api_key = bin2hex(random_bytes(16)); $model->api_key = bin2hex(random_bytes(16));
$model->api_secret = bin2hex(random_bytes(32)); $model->api_secret = bin2hex(random_bytes(32));
DB::connection('primary')->table($model->getTable()) DB::connection('primary')->table($model->getTable())->where('id', $model->id)->update([
->where('idn', $model->idn ?? $model->id) 'api_key' => $model->api_key,
->update([ 'api_secret' => $model->api_secret
'api_key' => $model->api_key, ]);
'api_secret' => $model->api_secret
]);
} }
} }