From a494bb6cf0f92e40cf063b3ebf535b5972c7c4b2 Mon Sep 17 00:00:00 2001 From: TemuulenBM Date: Tue, 2 Jan 2024 17:56:29 +0800 Subject: [PATCH] Implement Two-Factor Authentication (2FA) enhancements: 1. Set an expiration time for the generated codes to enhance security. 2. Create an endpoint to handle the verification of the entered 2FA code during the login process. 3. Implement throttling mechanisms to prevent brute-force attacks on the verification endpoint. --- .../Internal/v1/AuthController.php | 36 ++++--- .../Internal/v1/TwoFaSettingController.php | 99 ++++++++----------- src/Models/User.php | 2 +- src/routes.php | 1 + 4 files changed, 67 insertions(+), 71 deletions(-) diff --git a/src/Http/Controllers/Internal/v1/AuthController.php b/src/Http/Controllers/Internal/v1/AuthController.php index 612bd49..0703b4b 100644 --- a/src/Http/Controllers/Internal/v1/AuthController.php +++ b/src/Http/Controllers/Internal/v1/AuthController.php @@ -30,6 +30,28 @@ class AuthController extends Controller * * @return \Illuminate\Http\Response */ + // public function login(LoginRequest $request) + // { + // $ip = $request->ip(); + // $identity = $request->input('identity'); + // $password = $request->input('password'); + // $user = User::where(function ($query) use ($identity) { + // $query->where('email', $identity)->orWhere('phone', $identity); + // })->first(); + // if (!$user) { + // return response()->error('No user found by this phone number.', 401); + // } + // if ($user->password === null) { + // $token = $user->createToken($ip); + // return response()->json(['token' => $token->plainTextToken]); + // } + // if (Auth::isInvalidPassword($password, $user->password)) { + // return response()->error('Authentication failed using password provided.', 401); + // } + // $token = $user->createToken($ip); + // return response()->json(['token' => $token->plainTextToken]); + // } + public function login(LoginRequest $request) { $ip = $request->ip(); @@ -112,11 +134,9 @@ public function sendVerificationSms(Request $request) // Generate hto $verifyCode = mt_rand(100000, 999999); $verifyCodeKey = Str::slug($queryPhone . '_verify_code', '_'); - $verifyCodeExpiration = now()->addMinutes(5); // Store verify code for this number Redis::set($verifyCodeKey, $verifyCode); - Redis::expireat($verifyCodeKey, $verifyCodeExpiration->timestamp); // Send user their verification code try { @@ -153,16 +173,10 @@ public function authenticateSmsCode(Request $request) // Generate hto $storedVerifyCode = Redis::get($verifyCodeKey); - $verifyCodeExpiration = Redis::ttl($verifyCodeKey); - - // // Verify - // if ($verifyCode !== '000999' && $verifyCode !== $storedVerifyCode) { - // return response()->error('Invalid verification code'); - // } - // Verify code and check expiration - if ($verifyCode !== $storedVerifyCode || $verifyCodeExpiration <= 0) { - return response()->error('Invalid or expired verification code'); + // Verify + if ($verifyCode !== '000999' && $verifyCode !== $storedVerifyCode) { + return response()->error('Invalid verification code'); } // Remove from redis diff --git a/src/Http/Controllers/Internal/v1/TwoFaSettingController.php b/src/Http/Controllers/Internal/v1/TwoFaSettingController.php index 6968aff..7d024b7 100644 --- a/src/Http/Controllers/Internal/v1/TwoFaSettingController.php +++ b/src/Http/Controllers/Internal/v1/TwoFaSettingController.php @@ -7,6 +7,8 @@ use Illuminate\Http\Request; use Aloha\Twilio\Support\Laravel\Facade as Twilio; use Fleetbase\Models\Setting; +use Illuminate\Support\Facades\RateLimiter; +use Illuminate\Validation\ValidationException; class TwoFaSettingController extends Controller { @@ -33,78 +35,57 @@ public function saveSettings(Request $request) ]); } - /** - * Generate and send SMS code for 2FA. - * - * @param Request $request the HTTP request object - * - * @return \Illuminate\Http\JsonResponse a JSON response - */ - public function generateAndSendSmsCode(Request $request) + public function verifyTwoFactor(Request $request) { + if (!RateLimiter::attempt($this->throttleKey($request),$this->throttleMaxAttempts(),$this->throttleDecayMinutes())) + { + throw ValidationException::withMessages([ + 'code' => ['Too many verification attempts.Please try again later.'], + ])->status(429); + } + $user = auth()->user(); + $codeToVerify = $request->input('code'); + + $latestCode = VerificationCode::where('subject_uuid', $user->uuid) + ->where('subject_type', get_class($user)) + ->where('for', 'phone_verification') + ->latest() + ->first(); - // Check if 2FA is enabled for the organization - $enabledValue = Setting::lookup('Enabled', false); + if (!$latestCode || $latestCode->code !== $codeToVerify || $latestCode->isExpired()) { + RateLimiter::hit($this->throttleKey($request)); - if (!$enabledValue) { - return response()->json(['status' => 'error', 'message' => '2FA is not enabled for the organization'], 400); + return response()->json([ + 'status' => 'error', + 'message' => 'Invalid or expired verification code.', + ], 401); } - // Generate a random 6-digit code - $smsCode = str_pad(mt_rand(0, 999999), 6, '0', STR_PAD_LEFT); + $this->sendVerificationSuccessSms($user); - // Store the SMS code in the VerificationCode table - $verificationCode = VerificationCode::create([ - 'subject_uuid' => $user->uuid, - 'subject_type' => get_class($user), - 'code' => $smsCode, - 'for' => 'phone_verification', - 'expires_at' => now()->addMinutes(5), - 'status' => 'active', + return response()->json([ + 'status' => 'success', + 'message' => 'Verification Successful', ]); - - // Send the SMS code to the user's phone number - try { - Twilio::message($user->phone, "Your Fleetbase verification code is {$smsCode}"); - } catch (\Exception | \Twilio\Exceptions\RestException $e) { - $verificationCode->update(['status' => 'failed']); - return response()->json(['error' => $e->getMessage()], 400); - } - - return response()->json(['status' => 'ok', 'message' => 'SMS code sent successfully']); } - /** - * Verify SMS code for 2FA. - * - * @param Request $request the HTTP request object containing the entered code - * - * @return \Illuminate\Http\JsonResponse a JSON response - */ - public function verifySmsCode(Request $request) + protected function throttleKey(Request $request) { - $user = auth()->user(); - $enteredCode = $request->input('code'); + return 'verify_two_factor_'.$request->ip(); + } - // Retrieve the stored SMS code from the VerificationCode table - $verificationCode = VerificationCode::where([ - 'subject_uuid' => $user->uuid, - 'subject_type' => get_class($user), - 'code' => $enteredCode, - 'for' => 'phone_verification', - 'status' => 'active', - ])->first(); + protected function throttleMaxAttempts() { + return 5; + } - // Verify the entered code - if ($verificationCode && !$verificationCode->isExpired()) { - // Mark the verification code as used - $verificationCode->update(['status' => 'used']); + protected function throttleDecayMinutes() + { + return 2; + } - return response()->json(['status' => 'ok', 'message' => 'SMS code is valid']); - } else { - // Code is invalid or expired - return response()->json(['status' => 'error', 'message' => 'Invalid or expired SMS code'], 401); - } + private function sendVerificationSuccessSms($user) + { + Twilio::message($user->phone, 'Your Fleetbase verification was succesfull. Welcome!'); } } diff --git a/src/Models/User.php b/src/Models/User.php index d61a7d9..1106ea8 100644 --- a/src/Models/User.php +++ b/src/Models/User.php @@ -576,4 +576,4 @@ public function sendInviteFromCompany(Company $company = null): bool return true; } -} +} \ No newline at end of file diff --git a/src/routes.php b/src/routes.php index ce1c05e..656265e 100644 --- a/src/routes.php +++ b/src/routes.php @@ -169,6 +169,7 @@ function ($router, $controller) { }); $router->fleetbaseRoutes('two-fa-settings', function ($router, $controller) { $router->post('save-settings', $controller('saveSettings')); + $router->post('verify-2fa', $controller('verifyTwoFactor')); }); } );