From f8dbc99019f95c186bc3a3a95567e977b1b6ff46 Mon Sep 17 00:00:00 2001 From: TemuulenBM Date: Thu, 21 Dec 2023 18:04:26 +0800 Subject: [PATCH 01/22] Implemented SMS verifaction method on login --- .../Internal/v1/AuthController.php | 22 ++++++++++--- src/Models/Company.php | 1 + src/Models/User.php | 32 +++++++++++++++++++ src/Support/Utils.php | 30 +++++++++++++++++ 4 files changed, 81 insertions(+), 4 deletions(-) diff --git a/src/Http/Controllers/Internal/v1/AuthController.php b/src/Http/Controllers/Internal/v1/AuthController.php index 15508a1..566b2d1 100644 --- a/src/Http/Controllers/Internal/v1/AuthController.php +++ b/src/Http/Controllers/Internal/v1/AuthController.php @@ -45,6 +45,12 @@ public function login(LoginRequest $request) return response()->error('Authentication failed using password provided.', 401); } + if($user->isTwoFactorEnabled()) { + $this->sendVerificationSms($request); + + return response()->json(['status' => '2FA_required']); + } + $token = $user->createToken($ip); return response()->json(['token' => $token->plainTextToken]); @@ -112,14 +118,16 @@ 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 { Twilio::message($queryPhone, shell_exec('Your Fleetbase authentication code is ') . $verifyCode); - } catch (\Exception|\Twilio\Exceptions\RestException $e) { + } catch (\Exception | \Twilio\Exceptions\RestException $e) { return response()->json(['error' => $e->getMessage()], 400); } @@ -151,10 +159,16 @@ 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 + // 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'); } // Remove from redis diff --git a/src/Models/Company.php b/src/Models/Company.php index 07b0439..928e36d 100644 --- a/src/Models/Company.php +++ b/src/Models/Company.php @@ -3,6 +3,7 @@ namespace Fleetbase\Models; use Fleetbase\Casts\Json; +use Fleetbase\FleetOps\Models\Driver; use Fleetbase\Traits\HasApiModelBehavior; use Fleetbase\Traits\HasOptionsAttributes; use Fleetbase\Traits\HasPublicId; diff --git a/src/Models/User.php b/src/Models/User.php index d61a7d9..7bedbc4 100644 --- a/src/Models/User.php +++ b/src/Models/User.php @@ -190,6 +190,38 @@ class User extends Authenticatable */ protected static $logName = 'user'; + /** + * Check if Two-Factor Authentication (2FA) is enabled for the user. + * + * @return bool + */ + public function isTwoFactorEnabled() + { + return data_get($this->meta, 'two_factor_enabled', false); + } + + /** + * Enable Two-Factor Authentication (2FA) for the user. + * + * @return void + */ + public function enableTwoFactor() + { + $this->meta = array_merge($this->meta ?? [], ['two_factor_enabled' => true]); + $this->save(); + } + + /** + * Disable Two-Factor Authentication (2FA) for the user. + * + * @return void + */ + public function disableTwoFactor() + { + $this->meta = array_merge($this->meta ?? [], ['two_factor_enabled' => false]); + $this->save(); + } + /** * Get the options for generating the slug. */ diff --git a/src/Support/Utils.php b/src/Support/Utils.php index 1e51115..0ad2329 100644 --- a/src/Support/Utils.php +++ b/src/Support/Utils.php @@ -2197,4 +2197,34 @@ public static function addWwwToUrl($url) return $url; } + + public static function getModelCountry(\Illuminate\Database\Eloquent\Model $model): ?string + { + if (isset($model->country) && is_string($model->country)) { + if (strlen($model->country) === 2) { + return $model->country; + } + + $countryCode = static::getCountryCodeByName($model->country); + + if (strlen($countryCode) === 2) { + return $countryCode; + } + } + + if ($model instanceof Company) { + return null; + } + + // attempt to get country code from current company session + if (session()->has('company')) { + $company = Company::where('uuid', session('company'))->first(); + + if ($company) { + return static::getModelCountry($company); + } + } + + return null; + } } From 960466a4a1dc137607d4e327b70347af6d1aae84 Mon Sep 17 00:00:00 2001 From: TemuulenBM Date: Mon, 25 Dec 2023 18:03:54 +0800 Subject: [PATCH 02/22] created 2fa backend for saving --- .../Internal/v1/SettingController.php | 2 ++ .../Internal/v1/TwoFaSettingController.php | 36 +++++++++++++++++++ src/Models/Setting.php | 7 ++++ src/routes.php | 3 ++ 4 files changed, 48 insertions(+) create mode 100644 src/Http/Controllers/Internal/v1/TwoFaSettingController.php diff --git a/src/Http/Controllers/Internal/v1/SettingController.php b/src/Http/Controllers/Internal/v1/SettingController.php index a806155..7a4b04c 100644 --- a/src/Http/Controllers/Internal/v1/SettingController.php +++ b/src/Http/Controllers/Internal/v1/SettingController.php @@ -271,6 +271,8 @@ public function testQueueConfig(Request $request) return response()->json(['status' => $status, 'message' => $message]); } + + /** * Loads and sends the services configuration. * diff --git a/src/Http/Controllers/Internal/v1/TwoFaSettingController.php b/src/Http/Controllers/Internal/v1/TwoFaSettingController.php new file mode 100644 index 0000000..6b4bc3b --- /dev/null +++ b/src/Http/Controllers/Internal/v1/TwoFaSettingController.php @@ -0,0 +1,36 @@ +input('twoFaSettings'); + if (!is_array($twoFaSettings)) { + throw new \Exception('Invalid 2FA settings data.'); + } + Setting::configure('two_fa_settings', $twoFaSettings); + + return response()->json([ + 'status' => 'ok', + 'message' => '2Fa settings succesfully saved.', + ]); + } +} diff --git a/src/Models/Setting.php b/src/Models/Setting.php index 40c3233..e8b4a1c 100644 --- a/src/Models/Setting.php +++ b/src/Models/Setting.php @@ -2,11 +2,18 @@ namespace Fleetbase\Models; +use Fleetbase\Traits\Filterable; +use Fleetbase\Traits\HasApiModelBehavior; +use Fleetbase\Traits\Searchable; use Fleetbase\Casts\Json; use Illuminate\Database\Eloquent\Model as EloquentModel; class Setting extends EloquentModel { + use HasApiModelBehavior; + use Searchable; + use Filterable; + /** * Create a new instance of the model. * diff --git a/src/routes.php b/src/routes.php index fca5a05..ce1c05e 100644 --- a/src/routes.php +++ b/src/routes.php @@ -167,6 +167,9 @@ function ($router, $controller) { $router->delete('bulk-delete', $controller('bulkDelete')); $router->post('save-settings', $controller('saveSettings')); }); + $router->fleetbaseRoutes('two-fa-settings', function ($router, $controller) { + $router->post('save-settings', $controller('saveSettings')); + }); } ); } From bec41daefdf9dc5c8df8ae6a0db1be6c32273194 Mon Sep 17 00:00:00 2001 From: TemuulenBM Date: Tue, 26 Dec 2023 17:53:53 +0800 Subject: [PATCH 03/22] fixed 2fa key --- .../Internal/v1/AuthController.php | 6 -- .../Internal/v1/TwoFaSettingController.php | 84 +++++++++++++++++-- src/Models/User.php | 32 ------- 3 files changed, 79 insertions(+), 43 deletions(-) diff --git a/src/Http/Controllers/Internal/v1/AuthController.php b/src/Http/Controllers/Internal/v1/AuthController.php index 566b2d1..612bd49 100644 --- a/src/Http/Controllers/Internal/v1/AuthController.php +++ b/src/Http/Controllers/Internal/v1/AuthController.php @@ -45,12 +45,6 @@ public function login(LoginRequest $request) return response()->error('Authentication failed using password provided.', 401); } - if($user->isTwoFactorEnabled()) { - $this->sendVerificationSms($request); - - return response()->json(['status' => '2FA_required']); - } - $token = $user->createToken($ip); return response()->json(['token' => $token->plainTextToken]); diff --git a/src/Http/Controllers/Internal/v1/TwoFaSettingController.php b/src/Http/Controllers/Internal/v1/TwoFaSettingController.php index 6b4bc3b..6968aff 100644 --- a/src/Http/Controllers/Internal/v1/TwoFaSettingController.php +++ b/src/Http/Controllers/Internal/v1/TwoFaSettingController.php @@ -3,12 +3,11 @@ namespace Fleetbase\Http\Controllers\Internal\v1; use Fleetbase\Http\Controllers\Controller; -use Fleetbase\Models\Setting; +use Fleetbase\Models\VerificationCode; use Illuminate\Http\Request; +use Aloha\Twilio\Support\Laravel\Facade as Twilio; +use Fleetbase\Models\Setting; -/** - * Controller for managing two factor settings. - */ class TwoFaSettingController extends Controller { /** @@ -26,11 +25,86 @@ public function saveSettings(Request $request) if (!is_array($twoFaSettings)) { throw new \Exception('Invalid 2FA settings data.'); } - Setting::configure('two_fa_settings', $twoFaSettings); + Setting::configure('2fa', $twoFaSettings); return response()->json([ 'status' => 'ok', 'message' => '2Fa settings succesfully saved.', ]); } + + /** + * 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) + { + $user = auth()->user(); + + // Check if 2FA is enabled for the organization + $enabledValue = Setting::lookup('Enabled', false); + + if (!$enabledValue) { + return response()->json(['status' => 'error', 'message' => '2FA is not enabled for the organization'], 400); + } + + // Generate a random 6-digit code + $smsCode = str_pad(mt_rand(0, 999999), 6, '0', STR_PAD_LEFT); + + // 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', + ]); + + // 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) + { + $user = auth()->user(); + $enteredCode = $request->input('code'); + + // 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(); + + // Verify the entered code + if ($verificationCode && !$verificationCode->isExpired()) { + // Mark the verification code as used + $verificationCode->update(['status' => 'used']); + + 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); + } + } } diff --git a/src/Models/User.php b/src/Models/User.php index 7bedbc4..d61a7d9 100644 --- a/src/Models/User.php +++ b/src/Models/User.php @@ -190,38 +190,6 @@ class User extends Authenticatable */ protected static $logName = 'user'; - /** - * Check if Two-Factor Authentication (2FA) is enabled for the user. - * - * @return bool - */ - public function isTwoFactorEnabled() - { - return data_get($this->meta, 'two_factor_enabled', false); - } - - /** - * Enable Two-Factor Authentication (2FA) for the user. - * - * @return void - */ - public function enableTwoFactor() - { - $this->meta = array_merge($this->meta ?? [], ['two_factor_enabled' => true]); - $this->save(); - } - - /** - * Disable Two-Factor Authentication (2FA) for the user. - * - * @return void - */ - public function disableTwoFactor() - { - $this->meta = array_merge($this->meta ?? [], ['two_factor_enabled' => false]); - $this->save(); - } - /** * Get the options for generating the slug. */ From a494bb6cf0f92e40cf063b3ebf535b5972c7c4b2 Mon Sep 17 00:00:00 2001 From: TemuulenBM Date: Tue, 2 Jan 2024 17:56:29 +0800 Subject: [PATCH 04/22] 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')); }); } ); From ec8afefd67e2b1d28305624db34c66a6eb19a375 Mon Sep 17 00:00:00 2001 From: TemuulenBM Date: Wed, 3 Jan 2024 17:57:24 +0800 Subject: [PATCH 05/22] Created support class for 2FA --- .../Internal/v1/AuthController.php | 63 ++++---- .../Internal/v1/TwoFaController.php | 64 ++++++++ .../Internal/v1/TwoFaSettingController.php | 91 ----------- src/Support/TwoFactorAuth.php | 141 ++++++++++++++++++ src/routes.php | 11 +- 5 files changed, 249 insertions(+), 121 deletions(-) create mode 100644 src/Http/Controllers/Internal/v1/TwoFaController.php delete mode 100644 src/Http/Controllers/Internal/v1/TwoFaSettingController.php create mode 100644 src/Support/TwoFactorAuth.php diff --git a/src/Http/Controllers/Internal/v1/AuthController.php b/src/Http/Controllers/Internal/v1/AuthController.php index 0703b4b..213c289 100644 --- a/src/Http/Controllers/Internal/v1/AuthController.php +++ b/src/Http/Controllers/Internal/v1/AuthController.php @@ -17,6 +17,7 @@ use Fleetbase\Models\VerificationCode; use Fleetbase\Notifications\UserForgotPassword; use Fleetbase\Support\Auth; +use Fleetbase\Support\TwoFactorAuth; use Fleetbase\Support\Utils; use Illuminate\Http\Request; use Illuminate\Support\Carbon; @@ -30,48 +31,58 @@ 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(); - $email = $request->input('email'); + $identity = $request->input('identity'); $password = $request->input('password'); - $user = User::where('email', $email)->first(); + + $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 email.', 401); + return response()->error('No user found by this phone number.', 401); } + $token = $user->createToken($ip); + + // Check if 2FA enabled + if (TwoFactorAuth::isEnabled()) { + $twoFaSession = TwoFactorAuth::start(); + return response()->json(['two_fa_session' => $twoFaSession]); + } + + 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(); + // $email = $request->input('email'); + // $password = $request->input('password'); + // $user = User::where('email', $email)->first(); + + // if (!$user) { + // return response()->error('No user found by this email.', 401); + // } + + // 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]); + // } + /** * Takes a request username/ or email and password and attempts to authenticate user * will return the user model if the authentication was successful, else will 400. diff --git a/src/Http/Controllers/Internal/v1/TwoFaController.php b/src/Http/Controllers/Internal/v1/TwoFaController.php new file mode 100644 index 0000000..f5e26c1 --- /dev/null +++ b/src/Http/Controllers/Internal/v1/TwoFaController.php @@ -0,0 +1,64 @@ +twoFactorAuth = $twoFactorAuth; + } + + /** + * Save Two-Factor Authentication settings. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function saveSettings(Request $request) + { + return TwoFactorAuth::saveSettings($request); + } + + /** + * Get Two-Factor Authentication settings. + * + * @return \Illuminate\Http\JsonResponse + */ + public function getSettings() + { + return TwoFactorAuth::getSettings(); + } + + /** + * Verify Two-Factor Authentication code. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function verifyTwoFactor(Request $request) + { + return TwoFactorAuth::verifyTwoFactor($request); + } +} diff --git a/src/Http/Controllers/Internal/v1/TwoFaSettingController.php b/src/Http/Controllers/Internal/v1/TwoFaSettingController.php deleted file mode 100644 index 7d024b7..0000000 --- a/src/Http/Controllers/Internal/v1/TwoFaSettingController.php +++ /dev/null @@ -1,91 +0,0 @@ -input('twoFaSettings'); - if (!is_array($twoFaSettings)) { - throw new \Exception('Invalid 2FA settings data.'); - } - Setting::configure('2fa', $twoFaSettings); - - return response()->json([ - 'status' => 'ok', - 'message' => '2Fa settings succesfully saved.', - ]); - } - - 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(); - - if (!$latestCode || $latestCode->code !== $codeToVerify || $latestCode->isExpired()) { - RateLimiter::hit($this->throttleKey($request)); - - return response()->json([ - 'status' => 'error', - 'message' => 'Invalid or expired verification code.', - ], 401); - } - - $this->sendVerificationSuccessSms($user); - - return response()->json([ - 'status' => 'success', - 'message' => 'Verification Successful', - ]); - } - - protected function throttleKey(Request $request) - { - return 'verify_two_factor_'.$request->ip(); - } - - protected function throttleMaxAttempts() { - return 5; - } - - protected function throttleDecayMinutes() - { - return 2; - } - - private function sendVerificationSuccessSms($user) - { - Twilio::message($user->phone, 'Your Fleetbase verification was succesfull. Welcome!'); - } -} diff --git a/src/Support/TwoFactorAuth.php b/src/Support/TwoFactorAuth.php new file mode 100644 index 0000000..eb5726e --- /dev/null +++ b/src/Support/TwoFactorAuth.php @@ -0,0 +1,141 @@ +input('twoFaSettings'); + if (!is_array($twoFaSettings)) { + throw new \Exception('Invalid 2FA settings data.'); + } + Setting::configure('2fa', $twoFaSettings); + + return response()->json([ + 'status' => 'ok', + 'message' => '2Fa settings successfully saved.', + ]); + } + + /** + * Get Two-Factor Authentication settings. + * + * @return \Illuminate\Http\JsonResponse + */ + public static function getSettings() + { + $twoFaSettings = Setting::lookup('2fa', ['enabled' => false, 'method' => 'authenticator_app']); + + return response()->json($twoFaSettings); + } + + /** + * Verify Two-Factor Authentication code. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + */ + public static function verifyTwoFactor($request) + { + if (!RateLimiter::attempt(self::throttleKey($request), self::throttleMaxAttempts(), self::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(); + + if (!$latestCode || $latestCode->code !== $codeToVerify || $latestCode->isExpired()) { + RateLimiter::hit(self::throttleKey($request)); + + return response()->json([ + 'status' => 'error', + 'message' => 'Invalid or expired verification code.', + ], 401); + } + + self::sendVerificationSuccessSms($user); + + return response()->json([ + 'status' => 'success', + 'message' => 'Verification Successful', + ]); + } + + /** + * Get the throttle key based on the request's IP. + * + * @param \Illuminate\Http\Request $request + * @return string + */ + protected static function throttleKey($request) + { + return 'verify_two_factor_' . $request->ip(); + } + + /** + * Get the maximum number of attempts allowed in the throttle. + * + * @return int + */ + protected static function throttleMaxAttempts() + { + return 5; + } + + /** + * Get the decay time in minutes for the throttle. + * + * @return int + */ + protected static function throttleDecayMinutes() + { + return 2; + } + + /** + * Send a success SMS after successful verification. + * + * @param mixed $user + */ + private static function sendVerificationSuccessSms($user) + { + Twilio::message($user->phone, 'Your Fleetbase verification was successful. Welcome!'); + } + + public static function isEnabled() + { + return Setting::lookup('2fa', ['enabled']); + } + + public static function start() + { + return true; + } +} diff --git a/src/routes.php b/src/routes.php index 656265e..d29b858 100644 --- a/src/routes.php +++ b/src/routes.php @@ -118,6 +118,13 @@ function ($router, $controller) { $router->post('test-notification-channels-config', $controller('testNotificationChannelsConfig')); } ); + $router->fleetbaseRoutes( + 'two-fa', + function ($router, $controller) { + $router->post('settings', $controller('saveSettings')); + $router->get('settings', $controller('getSettings')); + } + ); $router->fleetbaseRoutes('api-events'); $router->fleetbaseRoutes('api-request-logs'); $router->fleetbaseRoutes( @@ -167,10 +174,6 @@ function ($router, $controller) { $router->delete('bulk-delete', $controller('bulkDelete')); $router->post('save-settings', $controller('saveSettings')); }); - $router->fleetbaseRoutes('two-fa-settings', function ($router, $controller) { - $router->post('save-settings', $controller('saveSettings')); - $router->post('verify-2fa', $controller('verifyTwoFactor')); - }); } ); } From 1ca998ddf5fb76b07dac02f44dbc1d718a2ec3d3 Mon Sep 17 00:00:00 2001 From: TemuulenBM Date: Thu, 4 Jan 2024 18:05:07 +0800 Subject: [PATCH 06/22] implemented 2fa validation --- .../Internal/v1/AuthController.php | 2 +- .../Internal/v1/TwoFaController.php | 26 +++++++++++ src/Support/TwoFactorAuth.php | 45 +++++++++++++++++-- src/routes.php | 1 + 4 files changed, 70 insertions(+), 4 deletions(-) diff --git a/src/Http/Controllers/Internal/v1/AuthController.php b/src/Http/Controllers/Internal/v1/AuthController.php index 213c289..95a4372 100644 --- a/src/Http/Controllers/Internal/v1/AuthController.php +++ b/src/Http/Controllers/Internal/v1/AuthController.php @@ -49,7 +49,7 @@ public function login(LoginRequest $request) // Check if 2FA enabled if (TwoFactorAuth::isEnabled()) { - $twoFaSession = TwoFactorAuth::start(); + $twoFaSession = TwoFactorAuth::start($user); return response()->json(['two_fa_session' => $twoFaSession]); } diff --git a/src/Http/Controllers/Internal/v1/TwoFaController.php b/src/Http/Controllers/Internal/v1/TwoFaController.php index f5e26c1..1e8c62f 100644 --- a/src/Http/Controllers/Internal/v1/TwoFaController.php +++ b/src/Http/Controllers/Internal/v1/TwoFaController.php @@ -61,4 +61,30 @@ public function verifyTwoFactor(Request $request) { return TwoFactorAuth::verifyTwoFactor($request); } + + public function checkTwoFactor(Request $request) + { + $identity = $request->input('identity'); + $isTwoFaEnabled = TwoFactorAuth::isEnabled(); + $twoFaSession = null; + $isTwoFaValidated = false; + $error = null; + + if ($isTwoFaEnabled) { + $twoFaSession = TwoFactorAuth::start($identity); + + if ($twoFaSession === null) { + $error = 'No user found using identity provided'; + } else { + $isTwoFaValidated = TwoFactorAuth::isTwoFactorSessionValidated($twoFaSession); + } + } + + return response()->json([ + 'isTwoFaEnabled' => $isTwoFaEnabled, + 'isTwoFaValidated' => $isTwoFaValidated, + 'twoFaSession' => $twoFaSession, + 'error' => $error + ]); + } } diff --git a/src/Support/TwoFactorAuth.php b/src/Support/TwoFactorAuth.php index eb5726e..ce613ec 100644 --- a/src/Support/TwoFactorAuth.php +++ b/src/Support/TwoFactorAuth.php @@ -5,7 +5,10 @@ use Fleetbase\Models\VerificationCode; use Aloha\Twilio\Support\Laravel\Facade as Twilio; use Fleetbase\Models\Setting; +use Fleetbase\Models\User; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\RateLimiter; +use Illuminate\Support\Str; use Illuminate\Validation\ValidationException; /** @@ -129,13 +132,49 @@ private static function sendVerificationSuccessSms($user) Twilio::message($user->phone, 'Your Fleetbase verification was successful. Welcome!'); } + /** + * Check if Two-Factor Authentication is enabled. + * + * @return bool + */ public static function isEnabled() { - return Setting::lookup('2fa', ['enabled']); + $twoFaSettings = Setting::lookup('2fa'); + + // dd(data_get($twoFaSettings, 'enabled')); + + return isset($twoFaSettings['enabled']) ? (bool)$twoFaSettings['enabled'] : false; + + // return data_get($twoFaSettings, 'enabled'); + } + + /** + * Start the Two-Factor Authentication process and return the session key. + * + * @return string + */ + public static function start(string $identity): ?string + { + $twoFaSession = Str::random(40); + + $user = User::where(function ($query) use ($identity) { + $query->where('email', $identity)->orWhere('phone', $identity); + })->first(); + + if ($user) { + Cache::put('two_fa_session:' . $user->uuid, true, now()->addMinutes(10)); + return $twoFaSession; + } + + return null; } - public static function start() + public static function isTwoFactorSessionValidated(?string $twoFaSession = null): bool { - return true; + if ($twoFaSession === null) { + return false; + } + // do check here + return false; } } diff --git a/src/routes.php b/src/routes.php index d29b858..dbc7682 100644 --- a/src/routes.php +++ b/src/routes.php @@ -123,6 +123,7 @@ function ($router, $controller) { function ($router, $controller) { $router->post('settings', $controller('saveSettings')); $router->get('settings', $controller('getSettings')); + $router->get('settings', $controller('checkTwoFactor')); } ); $router->fleetbaseRoutes('api-events'); From 21b7daa30fc411d0d6b1e789f54e244fa28153b1 Mon Sep 17 00:00:00 2001 From: TemuulenBM Date: Fri, 5 Jan 2024 18:03:00 +0800 Subject: [PATCH 07/22] Completed backend of validate session --- .../Internal/v1/AuthController.php | 20 -- .../Internal/v1/TwoFaController.php | 36 ++-- src/Http/Requests/TwoFaValidationRequest.php | 63 +++++++ src/Models/VerificationCode.php | 8 +- src/Support/AA.php | 173 ++++++++++++++++++ src/Support/TwoFactorAuth.php | 143 ++++++++------- src/routes.php | 12 +- 7 files changed, 340 insertions(+), 115 deletions(-) create mode 100644 src/Http/Requests/TwoFaValidationRequest.php create mode 100644 src/Support/AA.php diff --git a/src/Http/Controllers/Internal/v1/AuthController.php b/src/Http/Controllers/Internal/v1/AuthController.php index 95a4372..fe4ad02 100644 --- a/src/Http/Controllers/Internal/v1/AuthController.php +++ b/src/Http/Controllers/Internal/v1/AuthController.php @@ -63,26 +63,6 @@ public function login(LoginRequest $request) return response()->json(['token' => $token->plainTextToken]); } - // public function login(LoginRequest $request) - // { - // $ip = $request->ip(); - // $email = $request->input('email'); - // $password = $request->input('password'); - // $user = User::where('email', $email)->first(); - - // if (!$user) { - // return response()->error('No user found by this email.', 401); - // } - - // 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]); - // } - /** * Takes a request username/ or email and password and attempts to authenticate user * will return the user model if the authentication was successful, else will 400. diff --git a/src/Http/Controllers/Internal/v1/TwoFaController.php b/src/Http/Controllers/Internal/v1/TwoFaController.php index 1e8c62f..f9feaff 100644 --- a/src/Http/Controllers/Internal/v1/TwoFaController.php +++ b/src/Http/Controllers/Internal/v1/TwoFaController.php @@ -3,6 +3,7 @@ namespace Fleetbase\Http\Controllers\Internal\v1; use Fleetbase\Http\Controllers\Controller; +use Fleetbase\Http\Requests\TwoFaValidationRequest; use Illuminate\Http\Request; use Fleetbase\Support\TwoFactorAuth; @@ -57,34 +58,21 @@ public function getSettings() * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\JsonResponse */ - public function verifyTwoFactor(Request $request) + public function validateSession(Request $request) { - return TwoFactorAuth::verifyTwoFactor($request); + return TwoFactorAuth::validateSession(new TwoFaValidationRequest($request->all())); } + /** + * Check Two-Factor Authentication status for a given user identity. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + */ public function checkTwoFactor(Request $request) { - $identity = $request->input('identity'); - $isTwoFaEnabled = TwoFactorAuth::isEnabled(); - $twoFaSession = null; - $isTwoFaValidated = false; - $error = null; - - if ($isTwoFaEnabled) { - $twoFaSession = TwoFactorAuth::start($identity); - - if ($twoFaSession === null) { - $error = 'No user found using identity provided'; - } else { - $isTwoFaValidated = TwoFactorAuth::isTwoFactorSessionValidated($twoFaSession); - } - } - - return response()->json([ - 'isTwoFaEnabled' => $isTwoFaEnabled, - 'isTwoFaValidated' => $isTwoFaValidated, - 'twoFaSession' => $twoFaSession, - 'error' => $error - ]); + $result = TwoFactorAuth::checkTwoFactorStatus($request); + + return response()->json($result); } } diff --git a/src/Http/Requests/TwoFaValidationRequest.php b/src/Http/Requests/TwoFaValidationRequest.php new file mode 100644 index 0000000..bb5da49 --- /dev/null +++ b/src/Http/Requests/TwoFaValidationRequest.php @@ -0,0 +1,63 @@ +errors(); + $response = [ + 'errors' => [$errors->first()], + ]; + // if more than one error display the others + if ($errors->count() > 1) { + $response['errors'] = collect($errors->all()) + ->values() + ->toArray(); + } + + return response()->json($response, 422); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + return [ + 'token' => 'required', + 'identity' => 'required|email|exists:users,email', + ]; + } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + public function messages() + { + return [ + 'identity.required' => 'Email or phone number is required', + 'identity.exists' => 'No user found by this email', + 'identity.email' => 'Email used is invalid', + 'identity.phone' => 'Phone Number used is invalid', + 'token.required' => 'A two factor session token is required', + ]; + } +} diff --git a/src/Models/VerificationCode.php b/src/Models/VerificationCode.php index c315df0..b6df8d1 100644 --- a/src/Models/VerificationCode.php +++ b/src/Models/VerificationCode.php @@ -92,10 +92,10 @@ public static function generateFor($subject = null, $for = 'general_verification } /** static method to generate code for email verification */ - public static function generateEmailVerificationFor($subject, $for = 'email_verification', \Closure $messageCallback = null, $meta = []) + public static function generateEmailVerificationFor($subject, $for = 'email_verification', \Closure $messageCallback = null, $meta = [], $expireAfter = null) { $verifyCode = static::generateFor($subject, $for, false); - $verifyCode->expires_at = Carbon::now()->addHour(); + $verifyCode->expires_at = $expireAfter === null ? Carbon::now()->addHour() : $expireAfter; $verifyCode->meta = $meta; $verifyCode->save(); @@ -109,10 +109,10 @@ public static function generateEmailVerificationFor($subject, $for = 'email_veri } /** static method to generate code for phone verification */ - public static function generateSmsVerificationFor($subject, $for = 'phone_verification', \Closure $messageCallback = null, $meta = []) + public static function generateSmsVerificationFor($subject, $for = 'phone_verification', \Closure $messageCallback = null, $meta = [], $expireAfter = null) { $verifyCode = static::generateFor($subject, $for, false); - $verifyCode->expires_at = Carbon::now()->addHour(); + $verifyCode->expires_at = $expireAfter === null ? Carbon::now()->addHour() : $expireAfter; $verifyCode->meta = $meta; $verifyCode->save(); diff --git a/src/Support/AA.php b/src/Support/AA.php new file mode 100644 index 0000000..b32b0d9 --- /dev/null +++ b/src/Support/AA.php @@ -0,0 +1,173 @@ +input('twoFaSettings'); + if (!is_array($twoFaSettings)) { + throw new \Exception('Invalid 2FA settings data.'); + } + Setting::configure('2fa', $twoFaSettings); + + return response()->json([ + 'status' => 'ok', + 'message' => '2Fa settings successfully saved.', + ]); + } + + /** + * Get Two-Factor Authentication settings. + * + * @return \Illuminate\Http\JsonResponse + */ + public static function getSettings() + { + $twoFaSettings = Setting::lookup('2fa', ['enabled' => false, 'method' => 'authenticator_app']); + + return response()->json($twoFaSettings); + } + + /** + * Verify Two-Factor Authentication code. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + */ + public static function validateSession($request) + { + try { + $request->validate([ + 'token' => 'required', + ]); + + if (!self::isEnabled()) { + return response()->error('Two Factor Authentication is not enabled.', 400); + } + + $identity = $request->input('identity'); + + $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); + } + + VerificationCode::generateSmsVerificationFor($user); + + return response()->json(['status' => 'ok']); + } catch (ValidationException $e) { + return response()->error($e->getMessage(), 400); + } catch (\Exception $e) { + return response()->error($e->getMessage(), 500); + } + } + + /** + * Check Two-Factor Authentication status for a given user identity. + * + * @param \Illuminate\Http\Request $request + * @return array + */ + public static function checkTwoFactorStatus(Request $request) + { + $identity = $request->input('identity'); + $isTwoFaEnabled = self::isEnabled(); + $twoFaSession = null; + $isTwoFaValidated = false; + $error = null; + + if ($isTwoFaEnabled) { + $twoFaSession = self::start($identity); + + if ($twoFaSession === null) { + $error = 'No user found using the provided identity'; + } else { + $isTwoFaValidated = self::isTwoFactorSessionValidated($twoFaSession); + } + } + + return [ + 'isTwoFaEnabled' => $isTwoFaEnabled, + 'isTwoFaValidated' => $isTwoFaValidated, + 'twoFaSession' => $twoFaSession, + 'error' => $error + ]; + } + + /** + * Check if Two-Factor Authentication is enabled. + * + * @return bool + */ + public static function isEnabled() + { + $twoFaSettings = Setting::lookup('2fa'); + + return isset($twoFaSettings['enabled']) ? (bool)$twoFaSettings['enabled'] : false; + } + + /** + * Start the Two-Factor Authentication process and return the session key. + * + * @return string + */ + public static function start(string $identity): ?string + { + $twoFaSession = Str::random(40); + + $user = User::where(function ($query) use ($identity) { + $query->where('email', $identity)->orWhere('phone', $identity); + })->first(); + + if ($user) { + Cache::put('two_fa_session:' . $user->uuid, true, now()->addMinutes(10)); + return $twoFaSession; + } + + return null; + } + + /** + * Check if the Two-Factor Authentication session is validated. + * + * @param string|null $twoFaSession - The Two-Factor Authentication session key + * @return bool - True if the session is validated, false otherwise + */ + public static function isTwoFactorSessionValidated(?string $twoFaSession = null): bool + { + if ($twoFaSession === null) { + return false; + } + // do check here + return false; + } + +} \ No newline at end of file diff --git a/src/Support/TwoFactorAuth.php b/src/Support/TwoFactorAuth.php index ce613ec..1b5057d 100644 --- a/src/Support/TwoFactorAuth.php +++ b/src/Support/TwoFactorAuth.php @@ -3,13 +3,13 @@ namespace Fleetbase\Support; use Fleetbase\Models\VerificationCode; -use Aloha\Twilio\Support\Laravel\Facade as Twilio; +use Fleetbase\Http\Requests\TwoFaValidationRequest; use Fleetbase\Models\Setting; use Fleetbase\Models\User; use Illuminate\Support\Facades\Cache; -use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\Str; -use Illuminate\Validation\ValidationException; +use Illuminate\Http\Request; +use Illuminate\Support\Carbon; /** * Class TwoFactorAuth @@ -54,82 +54,94 @@ public static function getSettings() /** * Verify Two-Factor Authentication code. * - * @param \Illuminate\Http\Request $request + * @param \Fleetbase\Http\Requests\TwoFaValidationRequest $request * @return \Illuminate\Http\JsonResponse */ - public static function verifyTwoFactor($request) + public static function validateSession(TwoFaValidationRequest $request) { - if (!RateLimiter::attempt(self::throttleKey($request), self::throttleMaxAttempts(), self::throttleDecayMinutes())) { - throw ValidationException::withMessages([ - 'code' => ['Too many verification attempts. Please try again later.'], - ])->status(429); + if (!self::isEnabled()) { + return response()->error('Two Factor Authentication is not enabled.', 400); } + + $token = $request->input('token'); + $identity = $request->input('identity'); - $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(); + $user = User::where(function ($query) use ($identity) { + $query->where('email', $identity)->orWhere('phone', $identity); + })->first(); - if (!$latestCode || $latestCode->code !== $codeToVerify || $latestCode->isExpired()) { - RateLimiter::hit(self::throttleKey($request)); + if ($user) { + $twoFaSessionKey = 'two_fa_session:' . $user->uuid . ':' . $token; - return response()->json([ - 'status' => 'error', - 'message' => 'Invalid or expired verification code.', - ], 401); + if (Cache::has($twoFaSessionKey)) { + // send two factor code + static::sendVerificationCode($user); + return response()->json(['status' => 'ok']); + } } - self::sendVerificationSuccessSms($user); - return response()->json([ - 'status' => 'success', - 'message' => 'Verification Successful', - ]); + 'errors' => ['Two factor authentication session is invalid'] + ], 400); } - /** - * Get the throttle key based on the request's IP. - * - * @param \Illuminate\Http\Request $request - * @return string - */ - protected static function throttleKey($request) + public static function sendVerificationCode(User $user): void { - return 'verify_two_factor_' . $request->ip(); - } + $twoFaSettings = Setting::lookup('2fa'); + $method = data_get($twoFaSettings, 'method'); + $expireAfter = Carbon::now()->addMinutes(20); - /** - * Get the maximum number of attempts allowed in the throttle. - * - * @return int - */ - protected static function throttleMaxAttempts() - { - return 5; - } + if ($method === 'sms') { + // if user has no phone number throw error + if (!$user->phone) { + throw new \Exception('No phone number to send 2FA code to.'); + } - /** - * Get the decay time in minutes for the throttle. - * - * @return int - */ - protected static function throttleDecayMinutes() - { - return 2; + // create verification code + VerificationCode::generateSmsVerificationFor($user, '2fa', null, [], $expireAfter); + } + + if ($method === 'email') { + // if user has no phone number throw error + if (!$user->email) { + throw new \Exception('No email to send 2FA code to.'); + } + + // create verification code + VerificationCode::generateEmailVerificationFor($user, '2fa', null, [], $expireAfter); + } } /** - * Send a success SMS after successful verification. + * Check Two-Factor Authentication status for a given user identity. * - * @param mixed $user + * @param \Illuminate\Http\Request $request + * @return array */ - private static function sendVerificationSuccessSms($user) + public static function checkTwoFactorStatus(Request $request) { - Twilio::message($user->phone, 'Your Fleetbase verification was successful. Welcome!'); + $identity = $request->input('identity'); + $isTwoFaEnabled = self::isEnabled(); + $twoFaSession = null; + $isTwoFaValidated = false; + $error = null; + + if ($isTwoFaEnabled) { + $twoFaSession = self::start($identity); + + if ($twoFaSession === null) { + $error = 'No user found using the provided identity'; + } else { + $isTwoFaValidated = self::isTwoFactorSessionValidated($twoFaSession); + } + } + + return [ + 'isTwoFaEnabled' => $isTwoFaEnabled, + 'isTwoFaValidated' => $isTwoFaValidated, + 'twoFaSession' => $twoFaSession, + 'error' => $error + ]; } /** @@ -141,11 +153,7 @@ public static function isEnabled() { $twoFaSettings = Setting::lookup('2fa'); - // dd(data_get($twoFaSettings, 'enabled')); - return isset($twoFaSettings['enabled']) ? (bool)$twoFaSettings['enabled'] : false; - - // return data_get($twoFaSettings, 'enabled'); } /** @@ -162,14 +170,21 @@ public static function start(string $identity): ?string })->first(); if ($user) { - Cache::put('two_fa_session:' . $user->uuid, true, now()->addMinutes(10)); + $twoFaSessionKey = 'two_fa_session:' . $user->uuid . ':' . $twoFaSession; + Cache::put($twoFaSessionKey, $user->uuid, now()->addMinutes(10)); return $twoFaSession; } return null; } - public static function isTwoFactorSessionValidated(?string $twoFaSession = null): bool + /** + * Check if the Two-Factor Authentication session is validated. + * + * @param string|null $twoFaSession - The Two-Factor Authentication session key + * @return bool - True if the session is validated, false otherwise + */ + public static function isTwoFactorSessionValidated(?string $twoFaSession = null): bool { if ($twoFaSession === null) { return false; diff --git a/src/routes.php b/src/routes.php index dbc7682..2e38b6b 100644 --- a/src/routes.php +++ b/src/routes.php @@ -75,6 +75,13 @@ function ($router) { $router->get('branding', 'SettingController@getBrandingSettings'); } ); + $router->group( + ['prefix' => 'two-fa'], + function ($router) { + $router->get('check', 'TwoFaController@checkTwoFactor'); + $router->post('validate-session', 'TwoFaController@validateSession'); + } + ); $router->group( ['middleware' => ['fleetbase.protected']], function ($router) { @@ -121,9 +128,8 @@ function ($router, $controller) { $router->fleetbaseRoutes( 'two-fa', function ($router, $controller) { - $router->post('settings', $controller('saveSettings')); - $router->get('settings', $controller('getSettings')); - $router->get('settings', $controller('checkTwoFactor')); + $router->post('save-settings', $controller('saveSettings')); + $router->get('get-settings', $controller('getSettings')); } ); $router->fleetbaseRoutes('api-events'); From 2b402b64fd5f4959e243a4464c6d9bb0ff1734af Mon Sep 17 00:00:00 2001 From: TemuulenBM Date: Mon, 8 Jan 2024 18:05:43 +0800 Subject: [PATCH 08/22] implemented verify 2fa code logics on backend --- .../Internal/v1/TwoFaController.php | 17 +- src/Support/AA.php | 173 ------------------ src/Support/TwoFactorAuth.php | 74 +++++++- src/routes.php | 1 + 4 files changed, 83 insertions(+), 182 deletions(-) delete mode 100644 src/Support/AA.php diff --git a/src/Http/Controllers/Internal/v1/TwoFaController.php b/src/Http/Controllers/Internal/v1/TwoFaController.php index f9feaff..dd289fe 100644 --- a/src/Http/Controllers/Internal/v1/TwoFaController.php +++ b/src/Http/Controllers/Internal/v1/TwoFaController.php @@ -55,12 +55,12 @@ public function getSettings() /** * Verify Two-Factor Authentication code. * - * @param \Illuminate\Http\Request $request + * @param \Fleetbase\Http\Requests\TwoFaValidationRequest $request * @return \Illuminate\Http\JsonResponse */ - public function validateSession(Request $request) + public function validateSession(TwoFaValidationRequest $request) { - return TwoFactorAuth::validateSession(new TwoFaValidationRequest($request->all())); + return TwoFactorAuth::validateSession($request); } /** @@ -75,4 +75,15 @@ public function checkTwoFactor(Request $request) return response()->json($result); } + + /** + * Verify Two-Factor Authentication code. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function verifyCode(Request $request) { + return TwoFactorAuth::verifyCode($request); + } + } diff --git a/src/Support/AA.php b/src/Support/AA.php deleted file mode 100644 index b32b0d9..0000000 --- a/src/Support/AA.php +++ /dev/null @@ -1,173 +0,0 @@ -input('twoFaSettings'); - if (!is_array($twoFaSettings)) { - throw new \Exception('Invalid 2FA settings data.'); - } - Setting::configure('2fa', $twoFaSettings); - - return response()->json([ - 'status' => 'ok', - 'message' => '2Fa settings successfully saved.', - ]); - } - - /** - * Get Two-Factor Authentication settings. - * - * @return \Illuminate\Http\JsonResponse - */ - public static function getSettings() - { - $twoFaSettings = Setting::lookup('2fa', ['enabled' => false, 'method' => 'authenticator_app']); - - return response()->json($twoFaSettings); - } - - /** - * Verify Two-Factor Authentication code. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\JsonResponse - */ - public static function validateSession($request) - { - try { - $request->validate([ - 'token' => 'required', - ]); - - if (!self::isEnabled()) { - return response()->error('Two Factor Authentication is not enabled.', 400); - } - - $identity = $request->input('identity'); - - $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); - } - - VerificationCode::generateSmsVerificationFor($user); - - return response()->json(['status' => 'ok']); - } catch (ValidationException $e) { - return response()->error($e->getMessage(), 400); - } catch (\Exception $e) { - return response()->error($e->getMessage(), 500); - } - } - - /** - * Check Two-Factor Authentication status for a given user identity. - * - * @param \Illuminate\Http\Request $request - * @return array - */ - public static function checkTwoFactorStatus(Request $request) - { - $identity = $request->input('identity'); - $isTwoFaEnabled = self::isEnabled(); - $twoFaSession = null; - $isTwoFaValidated = false; - $error = null; - - if ($isTwoFaEnabled) { - $twoFaSession = self::start($identity); - - if ($twoFaSession === null) { - $error = 'No user found using the provided identity'; - } else { - $isTwoFaValidated = self::isTwoFactorSessionValidated($twoFaSession); - } - } - - return [ - 'isTwoFaEnabled' => $isTwoFaEnabled, - 'isTwoFaValidated' => $isTwoFaValidated, - 'twoFaSession' => $twoFaSession, - 'error' => $error - ]; - } - - /** - * Check if Two-Factor Authentication is enabled. - * - * @return bool - */ - public static function isEnabled() - { - $twoFaSettings = Setting::lookup('2fa'); - - return isset($twoFaSettings['enabled']) ? (bool)$twoFaSettings['enabled'] : false; - } - - /** - * Start the Two-Factor Authentication process and return the session key. - * - * @return string - */ - public static function start(string $identity): ?string - { - $twoFaSession = Str::random(40); - - $user = User::where(function ($query) use ($identity) { - $query->where('email', $identity)->orWhere('phone', $identity); - })->first(); - - if ($user) { - Cache::put('two_fa_session:' . $user->uuid, true, now()->addMinutes(10)); - return $twoFaSession; - } - - return null; - } - - /** - * Check if the Two-Factor Authentication session is validated. - * - * @param string|null $twoFaSession - The Two-Factor Authentication session key - * @return bool - True if the session is validated, false otherwise - */ - public static function isTwoFactorSessionValidated(?string $twoFaSession = null): bool - { - if ($twoFaSession === null) { - return false; - } - // do check here - return false; - } - -} \ No newline at end of file diff --git a/src/Support/TwoFactorAuth.php b/src/Support/TwoFactorAuth.php index 1b5057d..acaa0b3 100644 --- a/src/Support/TwoFactorAuth.php +++ b/src/Support/TwoFactorAuth.php @@ -2,6 +2,7 @@ namespace Fleetbase\Support; +use Carbon\Carbon as CarbonDateTime; use Fleetbase\Models\VerificationCode; use Fleetbase\Http\Requests\TwoFaValidationRequest; use Fleetbase\Models\Setting; @@ -62,21 +63,25 @@ public static function validateSession(TwoFaValidationRequest $request) if (!self::isEnabled()) { return response()->error('Two Factor Authentication is not enabled.', 400); } - + $token = $request->input('token'); $identity = $request->input('identity'); $user = User::where(function ($query) use ($identity) { $query->where('email', $identity)->orWhere('phone', $identity); - })->first(); + })->first(); if ($user) { $twoFaSessionKey = 'two_fa_session:' . $user->uuid . ':' . $token; + // create token creating info about the session + $expireAfter = Carbon::now()->addSeconds(61); + $clientSessionToken = base64_encode($expireAfter . '|' . Str::random()); + if (Cache::has($twoFaSessionKey)) { // send two factor code - static::sendVerificationCode($user); - return response()->json(['status' => 'ok']); + static::sendVerificationCode($user, $expireAfter); + return response()->json(['status' => 'ok', 'client_token' => $clientSessionToken]); } } @@ -85,11 +90,21 @@ public static function validateSession(TwoFaValidationRequest $request) ], 400); } - public static function sendVerificationCode(User $user): void + /** + * Send Two-Factor Authentication verification code. + * + * @param \Fleetbase\Models\User $user + * @throws \Exception + */ + public static function sendVerificationCode(User $user, ?CarbonDateTime $expireAfter): void { $twoFaSettings = Setting::lookup('2fa'); $method = data_get($twoFaSettings, 'method'); - $expireAfter = Carbon::now()->addMinutes(20); + + // if no expiration provided default to 1 min + if (!$expireAfter) { + $expireAfter = Carbon::now()->addSeconds(61); + } if ($method === 'sms') { // if user has no phone number throw error @@ -192,4 +207,51 @@ public static function isTwoFactorSessionValidated(?string $twoFaSession = null) // do check here return false; } + + /** + * Verify the Two-Factor Authentication code received via SMS. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + */ + public static function verifyCode(Request $request) + { + $token = $request->input('token'); + $identity = $request->input('identity'); + + $user = User::where(function ($query) use ($identity) { + $query->where('email', $identity)->orWhere('phone', $identity); + })->first(); + + if (!$user) { + return response()->json([ + 'errors' => ['No user found using the provided identity'] + ], 400); + } + + $twoFaSessionKey = 'two_fa_session:' . $user->uuid . ':' . $token; + + if (Cache::has($twoFaSessionKey)) { + $verificationCode = $request->input('verificationCode'); + + $verifyCode = VerificationCode::where('code', $verificationCode) + ->where('subject_uuid', $user->uuid) + ->where('status', 'active') + ->where('expires_at', '>', now()) + ->first(); + + if ($verifyCode) { + Cache::forget($twoFaSessionKey); + + // authenticate the user + $ip = $request->ip(); + $token = $user->createToken($ip); + + return response()->json(['auth_token' => $token->plainTextToken]); + } + } + return response()->json([ + 'errors' => ['Invalid verification code'] + ], 400); + } } diff --git a/src/routes.php b/src/routes.php index 2e38b6b..906cb7a 100644 --- a/src/routes.php +++ b/src/routes.php @@ -80,6 +80,7 @@ function ($router) { function ($router) { $router->get('check', 'TwoFaController@checkTwoFactor'); $router->post('validate-session', 'TwoFaController@validateSession'); + $router->post('verify-code', 'TwoFaController@verifyCode'); } ); $router->group( From f330ebeed4779a34ab473cd67ee1021dfb4d6cc7 Mon Sep 17 00:00:00 2001 From: TemuulenBM Date: Tue, 9 Jan 2024 18:45:39 +0800 Subject: [PATCH 09/22] completed 2fa resend and verify code function --- .../Internal/v1/AuthController.php | 11 ++ .../Internal/v1/TwoFaController.php | 77 +++++++-- src/Support/TwoFactorAuth.php | 147 +++++++++++++----- src/routes.php | 1 + 4 files changed, 184 insertions(+), 52 deletions(-) diff --git a/src/Http/Controllers/Internal/v1/AuthController.php b/src/Http/Controllers/Internal/v1/AuthController.php index fe4ad02..0009bf0 100644 --- a/src/Http/Controllers/Internal/v1/AuthController.php +++ b/src/Http/Controllers/Internal/v1/AuthController.php @@ -23,6 +23,7 @@ use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Redis; use Illuminate\Support\Str; +use Laravel\Sanctum\PersonalAccessToken; class AuthController extends Controller { @@ -36,6 +37,16 @@ public function login(LoginRequest $request) $ip = $request->ip(); $identity = $request->input('identity'); $password = $request->input('password'); + $authToken = $request->input('authToken'); + + // if attempting to authenticate with auth token validate it first against database and respond with it + if ($authToken) { + $personalAccessToken = PersonalAccessToken::findToken($authToken); + + if ($personalAccessToken) { + return response()->json(['token' => $authToken]); + } + } $user = User::where(function ($query) use ($identity) { $query->where('email', $identity)->orWhere('phone', $identity); diff --git a/src/Http/Controllers/Internal/v1/TwoFaController.php b/src/Http/Controllers/Internal/v1/TwoFaController.php index dd289fe..4eda005 100644 --- a/src/Http/Controllers/Internal/v1/TwoFaController.php +++ b/src/Http/Controllers/Internal/v1/TwoFaController.php @@ -39,7 +39,14 @@ public function __construct(TwoFactorAuth $twoFactorAuth) */ public function saveSettings(Request $request) { - return TwoFactorAuth::saveSettings($request); + try { + $result = $this->twoFactorAuth->saveSettings($request); + return response()->json($result); + } catch (\Exception $e) { + return response()->json([ + 'error' => $e->getMessage(), + ], 400); + } } /** @@ -49,7 +56,14 @@ public function saveSettings(Request $request) */ public function getSettings() { - return TwoFactorAuth::getSettings(); + try { + $result = $this->twoFactorAuth->getSettings(); + return response()->json($result); + } catch (\Exception $e) { + return response()->json([ + 'error' => $e->getMessage(), + ], 400); + } } /** @@ -60,7 +74,18 @@ public function getSettings() */ public function validateSession(TwoFaValidationRequest $request) { - return TwoFactorAuth::validateSession($request); + try { + $clientSessionToken = $this->twoFactorAuth->validateSession($request); + + return response()->json([ + 'status' => 'ok', + 'clientToken' => $clientSessionToken, + ]); + } catch (\Exception $e) { + return response()->json([ + 'error' => $e->getMessage(), + ], 400); + } } /** @@ -71,9 +96,15 @@ public function validateSession(TwoFaValidationRequest $request) */ public function checkTwoFactor(Request $request) { - $result = TwoFactorAuth::checkTwoFactorStatus($request); - - return response()->json($result); + try { + $result = $this->twoFactorAuth->checkTwoFactorStatus($request); + + return response()->json($result); + } catch (\Exception $e) { + return response()->json([ + 'error' => $e->getMessage(), + ], 400); + } } /** @@ -82,8 +113,36 @@ public function checkTwoFactor(Request $request) * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\JsonResponse */ - public function verifyCode(Request $request) { - return TwoFactorAuth::verifyCode($request); + public function verifyCode(Request $request) + { + try { + $authToken = $this->twoFactorAuth->verifyCode($request); + + return response()->json([ + 'authToken' => $authToken + ]); + } catch (\Exception $e) { + return response()->json([ + 'error' => $e->getMessage(), + ], 400); + } + } + + /** + * Resend Two-Factor Authentication verification code. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function resendCode(Request $request) + { + try { + $result = $this->twoFactorAuth->resendCode($request); + return response()->json($result); + } catch (\Exception $e) { + return response()->json([ + 'error' => $e->getMessage(), + ], 400); + } } - } diff --git a/src/Support/TwoFactorAuth.php b/src/Support/TwoFactorAuth.php index acaa0b3..43574bd 100644 --- a/src/Support/TwoFactorAuth.php +++ b/src/Support/TwoFactorAuth.php @@ -23,8 +23,8 @@ class TwoFactorAuth * Save Two-Factor Authentication settings. * * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\JsonResponse * @throws \Exception + * @return array */ public static function saveSettings($request) { @@ -34,38 +34,55 @@ public static function saveSettings($request) } Setting::configure('2fa', $twoFaSettings); - return response()->json([ + return [ 'status' => 'ok', 'message' => '2Fa settings successfully saved.', - ]); + ]; } /** * Get Two-Factor Authentication settings. * - * @return \Illuminate\Http\JsonResponse + * @return array */ public static function getSettings() { $twoFaSettings = Setting::lookup('2fa', ['enabled' => false, 'method' => 'authenticator_app']); - return response()->json($twoFaSettings); + return $twoFaSettings; } /** * Verify Two-Factor Authentication code. * * @param \Fleetbase\Http\Requests\TwoFaValidationRequest $request - * @return \Illuminate\Http\JsonResponse + * @throws \Exception + * @return array */ - public static function validateSession(TwoFaValidationRequest $request) + public static function validateSession(TwoFaValidationRequest $request): string { if (!self::isEnabled()) { - return response()->error('Two Factor Authentication is not enabled.', 400); + throw new \Exception('Two Factor Authentication is not enabled.'); } $token = $request->input('token'); $identity = $request->input('identity'); + $clientToken = $request->input('clientToken'); + + // IF ALREADY AN ACTIVE CLIENT TOKEN SESSION CHECK FOR VERIFICATION CODE SENT AND JUST RETURN CLIENT TOKEN + if ($clientToken) { + $clientTokenDecoded = base64_decode($clientToken); + $clientTokenParts = explode('|', $clientTokenDecoded); + $verificationCodeId = $clientTokenParts[1]; + + if ($verificationCodeId) { + $verificationCode = VerificationCode::where('uuid', $verificationCodeId)->exists(); + + if ($verificationCode) { + return $clientToken; + } + } + } $user = User::where(function ($query) use ($identity) { $query->where('email', $identity)->orWhere('phone', $identity); @@ -74,32 +91,30 @@ public static function validateSession(TwoFaValidationRequest $request) if ($user) { $twoFaSessionKey = 'two_fa_session:' . $user->uuid . ':' . $token; - // create token creating info about the session - $expireAfter = Carbon::now()->addSeconds(61); - $clientSessionToken = base64_encode($expireAfter . '|' . Str::random()); - if (Cache::has($twoFaSessionKey)) { - // send two factor code - static::sendVerificationCode($user, $expireAfter); - return response()->json(['status' => 'ok', 'client_token' => $clientSessionToken]); + // create token creating info about the session and send verification code + $expireAfter = Carbon::now()->addSeconds(61); + $verificationCode = static::sendVerificationCode($user, $expireAfter); + $clientSessionToken = base64_encode($expireAfter . '|' . $verificationCode->uuid . '|' . Str::random()); + + return $clientSessionToken; } } - return response()->json([ - 'errors' => ['Two factor authentication session is invalid'] - ], 400); + throw new \Exception('Two factor authentication session is invalid'); } /** * Send Two-Factor Authentication verification code. * * @param \Fleetbase\Models\User $user + * @param \Carbon\Carbon|null $expireAfter * @throws \Exception */ - public static function sendVerificationCode(User $user, ?CarbonDateTime $expireAfter): void + public static function sendVerificationCode(User $user, ?CarbonDateTime $expireAfter): VerificationCode { $twoFaSettings = Setting::lookup('2fa'); - $method = data_get($twoFaSettings, 'method'); + $method = data_get($twoFaSettings, 'method', 'email'); // if no expiration provided default to 1 min if (!$expireAfter) { @@ -113,7 +128,7 @@ public static function sendVerificationCode(User $user, ?CarbonDateTime $expireA } // create verification code - VerificationCode::generateSmsVerificationFor($user, '2fa', null, [], $expireAfter); + return VerificationCode::generateSmsVerificationFor($user, '2fa', null, [], $expireAfter); } if ($method === 'email') { @@ -123,7 +138,7 @@ public static function sendVerificationCode(User $user, ?CarbonDateTime $expireA } // create verification code - VerificationCode::generateEmailVerificationFor($user, '2fa', null, [], $expireAfter); + return VerificationCode::generateEmailVerificationFor($user, '2fa', null, [], $expireAfter); } } @@ -174,7 +189,8 @@ public static function isEnabled() /** * Start the Two-Factor Authentication process and return the session key. * - * @return string + * @param string $identity + * @return string|null */ public static function start(string $identity): ?string { @@ -212,46 +228,91 @@ public static function isTwoFactorSessionValidated(?string $twoFaSession = null) * Verify the Two-Factor Authentication code received via SMS. * * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\JsonResponse + * @throws \Exception + * @return array */ - public static function verifyCode(Request $request) + public static function verifyCode(Request $request): ?string { $token = $request->input('token'); $identity = $request->input('identity'); + $clientToken = $request->input('clientToken'); $user = User::where(function ($query) use ($identity) { $query->where('email', $identity)->orWhere('phone', $identity); })->first(); if (!$user) { - return response()->json([ - 'errors' => ['No user found using the provided identity'] - ], 400); + throw new \Exception('No user found using the provided identity'); } $twoFaSessionKey = 'two_fa_session:' . $user->uuid . ':' . $token; if (Cache::has($twoFaSessionKey)) { - $verificationCode = $request->input('verificationCode'); + $userInputCode = $request->input('verificationCode'); + + if ($clientToken) { + $clientTokenDecoded = base64_decode($clientToken); + $clientTokenParts = explode('|', $clientTokenDecoded); + $verificationCodeId = $clientTokenParts[1]; - $verifyCode = VerificationCode::where('code', $verificationCode) - ->where('subject_uuid', $user->uuid) - ->where('status', 'active') - ->where('expires_at', '>', now()) - ->first(); + if ($verificationCodeId) { + $verificationCode = VerificationCode::where('uuid', $verificationCodeId)->first(); - if ($verifyCode) { - Cache::forget($twoFaSessionKey); + if ($verificationCode) { + $verificationCodeMatches = $verificationCode->code === $userInputCode; - // authenticate the user - $ip = $request->ip(); - $token = $user->createToken($ip); + if ($verificationCodeMatches) { + Cache::forget($twoFaSessionKey); - return response()->json(['auth_token' => $token->plainTextToken]); + // authenticate the user + $ip = $request->ip(); + $token = $user->createToken($ip); + + return $token->plainTextToken; + } + } + } } } - return response()->json([ - 'errors' => ['Invalid verification code'] - ], 400); + + throw new \Exception('Invalid verification code'); + } + + /** + * Resend Two-Factor Authentication verification code. + * + * @param \Illuminate\Http\Request $request + * @return array + * @throws \Exception + */ + public static function resendCode(Request $request) + { + $token = $request->input('token'); + $identity = $request->input('identity'); + + $user = User::where(function ($query) use ($identity) { + $query->where('email', $identity)->orWhere('phone', $identity); + })->first(); + + if (!$user) { + throw new \Exception('No user found using the provided identity'); + } + + $twoFaSessionKey = 'two_fa_session:' . $user->uuid . ':' . $token; + + if (Cache::has($twoFaSessionKey)) { + Cache::forget($twoFaSessionKey); + + $expireAfter = Carbon::now()->addSeconds(61); + $newVerificationCode = static::sendVerificationCode($user, $expireAfter); + $newClientSessionToken = base64_encode($expireAfter . '|' . $newVerificationCode->uuid . '|' . Str::random()); + + return [ + 'status' => 'ok', + 'message' => 'Verification code resent successfully.', + 'newClientSessionToken' => $newClientSessionToken, + ]; + } + throw new \Exception('Two factor authentication session is invalid'); } } diff --git a/src/routes.php b/src/routes.php index 906cb7a..533e3c3 100644 --- a/src/routes.php +++ b/src/routes.php @@ -81,6 +81,7 @@ function ($router) { $router->get('check', 'TwoFaController@checkTwoFactor'); $router->post('validate-session', 'TwoFaController@validateSession'); $router->post('verify-code', 'TwoFaController@verifyCode'); + $router->post('resend-code', 'TwoFaController@resendCode'); } ); $router->group( From 2c32ef9f125509345991577b2af207a5e2dc0808 Mon Sep 17 00:00:00 2001 From: TemuulenBM Date: Wed, 10 Jan 2024 18:05:28 +0800 Subject: [PATCH 10/22] implemented resend code --- src/Support/TwoFactorAuth.php | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/Support/TwoFactorAuth.php b/src/Support/TwoFactorAuth.php index 43574bd..2563ea4 100644 --- a/src/Support/TwoFactorAuth.php +++ b/src/Support/TwoFactorAuth.php @@ -236,6 +236,8 @@ public static function verifyCode(Request $request): ?string $token = $request->input('token'); $identity = $request->input('identity'); $clientToken = $request->input('clientToken'); + $newClientSessionToken = $request->input('newClientSessionToken'); + $verificationCode = $request->input('verificationCode'); $user = User::where(function ($query) use ($identity) { $query->where('email', $identity)->orWhere('phone', $identity); @@ -268,6 +270,27 @@ public static function verifyCode(Request $request): ?string $ip = $request->ip(); $token = $user->createToken($ip); + return $token->plainTextToken; + } + } + } + } elseif ($newClientSessionToken) { + $newClientSessionTokenDecoded = base64_decode($newClientSessionToken); + $newClientSessionTokenParts = explode('|', $newClientSessionTokenDecoded); + $newVerificationCodeId = $newClientSessionTokenParts[1]; + + if ($newVerificationCodeId) { + $newVerificationCode = VerificationCode::where('uuid', $newVerificationCodeId)->first(); + + if ($newVerificationCode) { + $newVerificationCodeMatches = $newVerificationCode->code === $userInputCode; + + if ($newVerificationCodeMatches) { + Cache::forget($twoFaSessionKey); + + $ip = $request->ip(); + $token = $user->createToken($ip); + return $token->plainTextToken; } } From df4852634c37b159d371389e596b1fd133cf943e Mon Sep 17 00:00:00 2001 From: TemuulenBM Date: Fri, 12 Jan 2024 18:15:18 +0800 Subject: [PATCH 11/22] created backend of 2fa sms,email --- src/Support/TwoFactorAuth.php | 110 ++++++++++++++++++++-------------- 1 file changed, 66 insertions(+), 44 deletions(-) diff --git a/src/Support/TwoFactorAuth.php b/src/Support/TwoFactorAuth.php index 2563ea4..ef3a547 100644 --- a/src/Support/TwoFactorAuth.php +++ b/src/Support/TwoFactorAuth.php @@ -11,6 +11,7 @@ use Illuminate\Support\Str; use Illuminate\Http\Request; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Log; /** * Class TwoFactorAuth @@ -233,74 +234,94 @@ public static function isTwoFactorSessionValidated(?string $twoFaSession = null) */ public static function verifyCode(Request $request): ?string { - $token = $request->input('token'); - $identity = $request->input('identity'); - $clientToken = $request->input('clientToken'); - $newClientSessionToken = $request->input('newClientSessionToken'); - $verificationCode = $request->input('verificationCode'); + try { + $token = $request->input('token'); + $identity = $request->input('identity'); + $clientToken = $request->input('clientToken'); + $newClientSessionToken = $request->input('newClientSessionToken'); + $verificationCode = $request->input('verificationCode'); + + $user = User::where(function ($query) use ($identity) { + $query->where('email', $identity)->orWhere('phone', $identity); + })->first(); + + if (!$user) { + throw new \Exception('No user found using the provided identity'); + } - $user = User::where(function ($query) use ($identity) { - $query->where('email', $identity)->orWhere('phone', $identity); - })->first(); + $twoFaSessionKey = 'two_fa_session:' . $user->uuid . ':' . $token; - if (!$user) { - throw new \Exception('No user found using the provided identity'); - } + if (Cache::has($twoFaSessionKey)) { + $userInputCode = $request->input('verificationCode'); - $twoFaSessionKey = 'two_fa_session:' . $user->uuid . ':' . $token; + if ($clientToken) { + $clientTokenDecoded = base64_decode($clientToken); + $clientTokenParts = explode('|', $clientTokenDecoded); + $verificationCodeId = $clientTokenParts[1]; - if (Cache::has($twoFaSessionKey)) { - $userInputCode = $request->input('verificationCode'); + dd($verificationCodeId); - if ($clientToken) { - $clientTokenDecoded = base64_decode($clientToken); - $clientTokenParts = explode('|', $clientTokenDecoded); - $verificationCodeId = $clientTokenParts[1]; + if ($verificationCodeId) { + $verificationCode = VerificationCode::where('uuid', $verificationCodeId)->first(); - if ($verificationCodeId) { - $verificationCode = VerificationCode::where('uuid', $verificationCodeId)->first(); + if ($verificationCode) { + if ($verificationCode->expires_at < now()) { + throw new \Exception('Verification code has expired. Please request a new one.'); + } - if ($verificationCode) { - $verificationCodeMatches = $verificationCode->code === $userInputCode; + $verificationCodeMatches = $verificationCode->code === $userInputCode; - if ($verificationCodeMatches) { - Cache::forget($twoFaSessionKey); + if ($verificationCodeMatches) { + Cache::forget($twoFaSessionKey); - // authenticate the user - $ip = $request->ip(); - $token = $user->createToken($ip); + // authenticate the user + $ip = $request->ip(); + $token = $user->createToken($ip); - return $token->plainTextToken; + return $token->plainTextToken; + } else { + throw new \Exception('Verification code does not match. User entered: ' . $userInputCode . ', Expected: ' . $verificationCode->code); + } } } - } - } elseif ($newClientSessionToken) { - $newClientSessionTokenDecoded = base64_decode($newClientSessionToken); - $newClientSessionTokenParts = explode('|', $newClientSessionTokenDecoded); - $newVerificationCodeId = $newClientSessionTokenParts[1]; + } elseif ($newClientSessionToken) { + $newClientSessionTokenDecoded = base64_decode($newClientSessionToken); + $newClientSessionTokenParts = explode('|', $newClientSessionTokenDecoded); + $newVerificationCodeId = $newClientSessionTokenParts[1]; - if ($newVerificationCodeId) { - $newVerificationCode = VerificationCode::where('uuid', $newVerificationCodeId)->first(); + if ($newVerificationCodeId) { + $newVerificationCode = VerificationCode::where('uuid', $newVerificationCodeId)->first(); - if ($newVerificationCode) { - $newVerificationCodeMatches = $newVerificationCode->code === $userInputCode; + if ($newVerificationCode->expires_at < now()) { + throw new \Exception('Verification code has expired. Please request a new one.'); + } + + if ($newVerificationCode) { + $newVerificationCodeMatches = $newVerificationCode->code === $userInputCode; - if ($newVerificationCodeMatches) { - Cache::forget($twoFaSessionKey); + if ($newVerificationCodeMatches) { + Cache::forget($twoFaSessionKey); - $ip = $request->ip(); - $token = $user->createToken($ip); + $ip = $request->ip(); + $token = $user->createToken($ip); - return $token->plainTextToken; + return $token->plainTextToken; + } else { + throw new \Exception('Verification code does not match. User entered: ' . $userInputCode . ', Expected: ' . $newVerificationCode->code); + } } } } } - } - throw new \Exception('Invalid verification code'); + throw new \Exception('Invalid verification code'); + } catch (\Exception $e) { + \Illuminate\Support\Facades\Log::error('Error during verification: ' . $e->getMessage()); + throw $e; + } } + /** * Resend Two-Factor Authentication verification code. * @@ -334,6 +355,7 @@ public static function resendCode(Request $request) 'status' => 'ok', 'message' => 'Verification code resent successfully.', 'newClientSessionToken' => $newClientSessionToken, + 'generatedVerificationCode' => $newVerificationCode->code, ]; } throw new \Exception('Two factor authentication session is invalid'); From b6e7f10ae1812ff932405463c900d399a17d2a4a Mon Sep 17 00:00:00 2001 From: TemuulenBM Date: Mon, 15 Jan 2024 16:14:56 +0800 Subject: [PATCH 12/22] latest push --- src/Support/TwoFactorAuth.php | 77 +++++++++-------------------------- 1 file changed, 19 insertions(+), 58 deletions(-) diff --git a/src/Support/TwoFactorAuth.php b/src/Support/TwoFactorAuth.php index ef3a547..394fa49 100644 --- a/src/Support/TwoFactorAuth.php +++ b/src/Support/TwoFactorAuth.php @@ -11,7 +11,6 @@ use Illuminate\Support\Str; use Illuminate\Http\Request; use Illuminate\Support\Carbon; -use Illuminate\Support\Facades\Log; /** * Class TwoFactorAuth @@ -238,7 +237,6 @@ public static function verifyCode(Request $request): ?string $token = $request->input('token'); $identity = $request->input('identity'); $clientToken = $request->input('clientToken'); - $newClientSessionToken = $request->input('newClientSessionToken'); $verificationCode = $request->input('verificationCode'); $user = User::where(function ($query) use ($identity) { @@ -251,6 +249,7 @@ public static function verifyCode(Request $request): ?string $twoFaSessionKey = 'two_fa_session:' . $user->uuid . ':' . $token; + if (Cache::has($twoFaSessionKey)) { $userInputCode = $request->input('verificationCode'); @@ -259,8 +258,6 @@ public static function verifyCode(Request $request): ?string $clientTokenParts = explode('|', $clientTokenDecoded); $verificationCodeId = $clientTokenParts[1]; - dd($verificationCodeId); - if ($verificationCodeId) { $verificationCode = VerificationCode::where('uuid', $verificationCodeId)->first(); @@ -284,33 +281,6 @@ public static function verifyCode(Request $request): ?string } } } - } elseif ($newClientSessionToken) { - $newClientSessionTokenDecoded = base64_decode($newClientSessionToken); - $newClientSessionTokenParts = explode('|', $newClientSessionTokenDecoded); - $newVerificationCodeId = $newClientSessionTokenParts[1]; - - if ($newVerificationCodeId) { - $newVerificationCode = VerificationCode::where('uuid', $newVerificationCodeId)->first(); - - if ($newVerificationCode->expires_at < now()) { - throw new \Exception('Verification code has expired. Please request a new one.'); - } - - if ($newVerificationCode) { - $newVerificationCodeMatches = $newVerificationCode->code === $userInputCode; - - if ($newVerificationCodeMatches) { - Cache::forget($twoFaSessionKey); - - $ip = $request->ip(); - $token = $user->createToken($ip); - - return $token->plainTextToken; - } else { - throw new \Exception('Verification code does not match. User entered: ' . $userInputCode . ', Expected: ' . $newVerificationCode->code); - } - } - } } } @@ -321,43 +291,34 @@ public static function verifyCode(Request $request): ?string } } - - /** - * Resend Two-Factor Authentication verification code. - * - * @param \Illuminate\Http\Request $request - * @return array - * @throws \Exception - */ public static function resendCode(Request $request) { - $token = $request->input('token'); - $identity = $request->input('identity'); - - $user = User::where(function ($query) use ($identity) { - $query->where('email', $identity)->orWhere('phone', $identity); - })->first(); + try { + $identity = $request->input('identity'); + $twoFaSession = self::start($identity); - if (!$user) { - throw new \Exception('No user found using the provided identity'); - } + if ($twoFaSession === null) { + throw new \Exception('No user found using the provided identity'); + } - $twoFaSessionKey = 'two_fa_session:' . $user->uuid . ':' . $token; + $user = User::where(function ($query) use ($identity) { + $query->where('email', $identity)->orWhere('phone', $identity); + })->first(); - if (Cache::has($twoFaSessionKey)) { - Cache::forget($twoFaSessionKey); + if (!$user) { + throw new \Exception('No user found using the provided identity'); + } $expireAfter = Carbon::now()->addSeconds(61); - $newVerificationCode = static::sendVerificationCode($user, $expireAfter); - $newClientSessionToken = base64_encode($expireAfter . '|' . $newVerificationCode->uuid . '|' . Str::random()); + static::sendVerificationCode($user, $expireAfter); return [ - 'status' => 'ok', - 'message' => 'Verification code resent successfully.', - 'newClientSessionToken' => $newClientSessionToken, - 'generatedVerificationCode' => $newVerificationCode->code, + 'status' => 'ok', + 'message' => 'Verification code resent successfully.', ]; + } catch (\Exception $e) { + \Illuminate\Support\Facades\Log::error('Error during resendCode: ' . $e->getMessage()); + throw $e; } - throw new \Exception('Two factor authentication session is invalid'); } } From 5e9ebdaa4e1c05d3a066ede1391fabd6834ea031 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Mon, 15 Jan 2024 18:35:56 +0800 Subject: [PATCH 13/22] patch two fa implementation for resend code --- .../Internal/v1/TwoFaController.php | 8 ++++++-- src/Support/TwoFactorAuth.php | 19 ++++++++----------- src/routes.php | 4 ++-- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/Http/Controllers/Internal/v1/TwoFaController.php b/src/Http/Controllers/Internal/v1/TwoFaController.php index 4eda005..c1b31e1 100644 --- a/src/Http/Controllers/Internal/v1/TwoFaController.php +++ b/src/Http/Controllers/Internal/v1/TwoFaController.php @@ -137,8 +137,12 @@ public function verifyCode(Request $request) public function resendCode(Request $request) { try { - $result = $this->twoFactorAuth->resendCode($request); - return response()->json($result); + $clientSessionToken = $this->twoFactorAuth->resendCode($request); + + return response()->json([ + 'status' => 'ok', + 'clientToken' => $clientSessionToken, + ]); } catch (\Exception $e) { return response()->json([ 'error' => $e->getMessage(), diff --git a/src/Support/TwoFactorAuth.php b/src/Support/TwoFactorAuth.php index 394fa49..71c128a 100644 --- a/src/Support/TwoFactorAuth.php +++ b/src/Support/TwoFactorAuth.php @@ -249,7 +249,6 @@ public static function verifyCode(Request $request): ?string $twoFaSessionKey = 'two_fa_session:' . $user->uuid . ':' . $token; - if (Cache::has($twoFaSessionKey)) { $userInputCode = $request->input('verificationCode'); @@ -291,15 +290,15 @@ public static function verifyCode(Request $request): ?string } } - public static function resendCode(Request $request) + public static function resendCode(Request $request): string { try { $identity = $request->input('identity'); - $twoFaSession = self::start($identity); + // $twoFaSession = self::start($identity); - if ($twoFaSession === null) { - throw new \Exception('No user found using the provided identity'); - } + // if ($twoFaSession === null) { + // throw new \Exception('No user found using the provided identity'); + // } $user = User::where(function ($query) use ($identity) { $query->where('email', $identity)->orWhere('phone', $identity); @@ -310,12 +309,10 @@ public static function resendCode(Request $request) } $expireAfter = Carbon::now()->addSeconds(61); - static::sendVerificationCode($user, $expireAfter); + $verificationCode = static::sendVerificationCode($user, $expireAfter); + $clientSessionToken = base64_encode($expireAfter . '|' . $verificationCode->uuid . '|' . Str::random()); - return [ - 'status' => 'ok', - 'message' => 'Verification code resent successfully.', - ]; + return $clientSessionToken; } catch (\Exception $e) { \Illuminate\Support\Facades\Log::error('Error during resendCode: ' . $e->getMessage()); throw $e; diff --git a/src/routes.php b/src/routes.php index 7b968f4..7fa5d5e 100644 --- a/src/routes.php +++ b/src/routes.php @@ -148,8 +148,8 @@ function ($router, $controller) { $router->fleetbaseRoutes( 'two-fa', function ($router, $controller) { - $router->post('save-settings', $controller('saveSettings')); - $router->get('get-settings', $controller('getSettings')); + $router->post('settings', $controller('saveSettings')); + $router->get('settings', $controller('getSettings')); } ); $router->fleetbaseRoutes('api-events'); From bd7f3e70b777124ca06825a709150759bf045f14 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Tue, 16 Jan 2024 18:50:27 +0800 Subject: [PATCH 14/22] completed two factor auth support class refactor, added two factor setting endpoints for user and organization --- .../Internal/v1/AuthController.php | 17 +- .../Internal/v1/CompanyController.php | 40 + .../Internal/v1/SettingController.php | 2 - .../Internal/v1/TwoFaController.php | 150 ++-- .../Internal/v1/UserController.php | 38 + src/Http/Requests/TwoFaValidationRequest.php | 2 +- src/Mail/VerifyEmail.php | 56 +- src/Models/Setting.php | 27 +- src/Models/User.php | 14 +- src/Models/VerificationCode.php | 9 +- src/Notifications/UserForgotPassword.php | 2 +- src/Support/TwoFactorAuth.php | 694 +++++++++++++----- src/routes.php | 24 +- 13 files changed, 789 insertions(+), 286 deletions(-) diff --git a/src/Http/Controllers/Internal/v1/AuthController.php b/src/Http/Controllers/Internal/v1/AuthController.php index 0009bf0..6e2bd98 100644 --- a/src/Http/Controllers/Internal/v1/AuthController.php +++ b/src/Http/Controllers/Internal/v1/AuthController.php @@ -34,9 +34,9 @@ class AuthController extends Controller */ public function login(LoginRequest $request) { - $ip = $request->ip(); - $identity = $request->input('identity'); - $password = $request->input('password'); + $ip = $request->ip(); + $identity = $request->input('identity'); + $password = $request->input('password'); $authToken = $request->input('authToken'); // if attempting to authenticate with auth token validate it first against database and respond with it @@ -59,9 +59,13 @@ public function login(LoginRequest $request) $token = $user->createToken($ip); // Check if 2FA enabled - if (TwoFactorAuth::isEnabled()) { + if (TwoFactorAuth::isEnabled($user)) { $twoFaSession = TwoFactorAuth::start($user); - return response()->json(['two_fa_session' => $twoFaSession]); + + return response()->json([ + 'twoFaSession' => $twoFaSession, + 'isEnabled' => true, + ]); } return response()->json(['token' => $token->plainTextToken]); @@ -71,6 +75,7 @@ public function login(LoginRequest $request) } $token = $user->createToken($ip); + return response()->json(['token' => $token->plainTextToken]); } @@ -143,7 +148,7 @@ public function sendVerificationSms(Request $request) // Send user their verification code try { Twilio::message($queryPhone, shell_exec('Your Fleetbase authentication code is ') . $verifyCode); - } catch (\Exception | \Twilio\Exceptions\RestException $e) { + } catch (\Exception|\Twilio\Exceptions\RestException $e) { return response()->json(['error' => $e->getMessage()], 400); } diff --git a/src/Http/Controllers/Internal/v1/CompanyController.php b/src/Http/Controllers/Internal/v1/CompanyController.php index 4d53d31..8a8311a 100644 --- a/src/Http/Controllers/Internal/v1/CompanyController.php +++ b/src/Http/Controllers/Internal/v1/CompanyController.php @@ -6,6 +6,9 @@ use Fleetbase\Http\Resources\Organization; use Fleetbase\Models\Company; use Fleetbase\Models\Invite; +use Fleetbase\Support\Auth; +use Fleetbase\Support\TwoFactorAuth; +use Illuminate\Http\Request; use Illuminate\Support\Str; class CompanyController extends FleetbaseController @@ -39,4 +42,41 @@ public function findCompany(string $id) return new Organization($company); } + + /** + * Get the current organization's two factor authentication settings. + * + * @return \Illuminate\Http\Response + */ + public function getTwoFactorSettings() + { + $company = Auth::getCompany(); + + if (!$company) { + return response()->error('No company session found', 401); + } + + $twoFaSettings = TwoFactorAuth::getTwoFaSettingsForCompany($company); + + return response()->json($twoFaSettings->value); + } + + /** + * Save the current user's two factor authentication settings. + * + * @return \Illuminate\Http\Response + */ + public function saveTwoFactorSettings(Request $request) + { + $twoFaSettings = $request->array('twoFaSettings'); + $company = Auth::getCompany(); + + if (!$company) { + return response()->error('No company session found', 401); + } + + $twoFaSettings = TwoFactorAuth::saveTwoFaSettingsForCompany($company, $twoFaSettings); + + return response()->json($twoFaSettings->value); + } } diff --git a/src/Http/Controllers/Internal/v1/SettingController.php b/src/Http/Controllers/Internal/v1/SettingController.php index a379b06..9ffca05 100644 --- a/src/Http/Controllers/Internal/v1/SettingController.php +++ b/src/Http/Controllers/Internal/v1/SettingController.php @@ -271,8 +271,6 @@ public function testQueueConfig(Request $request) return response()->json(['status' => $status, 'message' => $message]); } - - /** * Loads and sends the services configuration. * diff --git a/src/Http/Controllers/Internal/v1/TwoFaController.php b/src/Http/Controllers/Internal/v1/TwoFaController.php index c1b31e1..14b78d0 100644 --- a/src/Http/Controllers/Internal/v1/TwoFaController.php +++ b/src/Http/Controllers/Internal/v1/TwoFaController.php @@ -4,149 +4,149 @@ use Fleetbase\Http\Controllers\Controller; use Fleetbase\Http\Requests\TwoFaValidationRequest; -use Illuminate\Http\Request; use Fleetbase\Support\TwoFactorAuth; +use Illuminate\Http\Request; +use Illuminate\Support\Str; /** - * Class TwoFaController - * - * @package Fleetbase\Http\Controllers\Internal\v1 + * Class TwoFaController. */ class TwoFaController extends Controller { /** - * TwoFactorAuth instance. - * - * @var \Fleetbase\Support\TwoFactorAuth - */ - protected $twoFactorAuth; - - /** - * TwoFaController constructor. + * Save Two-Factor Authentication system wide settings. * - * @param \Fleetbase\Support\TwoFactorAuth $twoFactorAuth + * @return \Illuminate\Http\Response */ - public function __construct(TwoFactorAuth $twoFactorAuth) + public function saveSystemConfig(Request $request) { - $this->twoFactorAuth = $twoFactorAuth; + $twoFaSettings = $request->array('twoFaSettings'); + $settings = TwoFactorAuth::configureTwoFaSettings($twoFaSettings); + + return response()->json($settings->value); } /** - * Save Two-Factor Authentication settings. + * Get Two-Factor Authentication system wide settings. * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\JsonResponse + * @return \Illuminate\Http\Response */ - public function saveSettings(Request $request) + public function getSystemConfig() { - try { - $result = $this->twoFactorAuth->saveSettings($request); - return response()->json($result); - } catch (\Exception $e) { - return response()->json([ - 'error' => $e->getMessage(), - ], 400); - } + $settings = TwoFactorAuth::getTwoFaConfiguration(); + + return response()->json($settings->value); } /** - * Get Two-Factor Authentication settings. + * Check Two-Factor Authentication status for a given user identity. * - * @return \Illuminate\Http\JsonResponse + * @return \Illuminate\Http\Response */ - public function getSettings() + public function checkTwoFactor(Request $request) { - try { - $result = $this->twoFactorAuth->getSettings(); - return response()->json($result); - } catch (\Exception $e) { - return response()->json([ - 'error' => $e->getMessage(), - ], 400); - } + $identity = $request->input('identity'); + $twoFaSession = TwoFactorAuth::createTwoFaSessionIfEnabled($identity); + $isTwoFaEnabled = $twoFaSession !== null; + + return response()->json([ + 'twoFaSession' => $twoFaSession, + 'isTwoFaEnabled' => $isTwoFaEnabled, + ]); } /** * Verify Two-Factor Authentication code. * - * @param \Fleetbase\Http\Requests\TwoFaValidationRequest $request - * @return \Illuminate\Http\JsonResponse + * @return \Illuminate\Http\Response */ public function validateSession(TwoFaValidationRequest $request) { + $token = $request->input('token'); + $identity = $request->input('identity'); + $clientToken = $request->input('clientToken'); + try { - $clientSessionToken = $this->twoFactorAuth->validateSession($request); + $validClientToken = TwoFactorAuth::getClientSessionTokenFromTwoFaSession($token, $identity, $clientToken); return response()->json([ - 'status' => 'ok', - 'clientToken' => $clientSessionToken, + 'clientToken' => $validClientToken, + 'expired' => false, ]); } catch (\Exception $e) { - return response()->json([ - 'error' => $e->getMessage(), - ], 400); + $errorMessage = $e->getMessage(); + + if (Str::contains($errorMessage, ['2FA Verification', 'expired'])) { + return response()->json([ + 'expired' => true, + ]); + } + + return response()->error($errorMessage); } } /** - * Check Two-Factor Authentication status for a given user identity. + * Verify Two-Factor Authentication code. * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\JsonResponse + * @return \Illuminate\Http\Response */ - public function checkTwoFactor(Request $request) + public function verifyCode(Request $request) { + $code = $request->input('code'); + $token = $request->input('token'); + $clientToken = $request->input('clientToken'); + try { - $result = $this->twoFactorAuth->checkTwoFactorStatus($request); + $authToken = TwoFactorAuth::verifyCode($code, $token, $clientToken); - return response()->json($result); - } catch (\Exception $e) { return response()->json([ - 'error' => $e->getMessage(), - ], 400); + 'authToken' => $authToken, + ]); + } catch (\Exception $e) { + return response()->error($e->getMessage()); } } /** - * Verify Two-Factor Authentication code. + * Resend Two-Factor Authentication verification code. * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\JsonResponse + * @return \Illuminate\Http\Response */ - public function verifyCode(Request $request) + public function resendCode(Request $request) { + $identity = $request->input('identity'); + $token = $request->input('token'); + try { - $authToken = $this->twoFactorAuth->verifyCode($request); + $clientToken = TwoFactorAuth::resendCode($identity, $token); return response()->json([ - 'authToken' => $authToken + 'clientToken' => $clientToken, ]); } catch (\Exception $e) { - return response()->json([ - 'error' => $e->getMessage(), - ], 400); + return response()->error($e->getMessage()); } } /** - * Resend Two-Factor Authentication verification code. + * Invalidate the current two-factor session. * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\JsonResponse + * @return \Illuminate\Http\Response */ - public function resendCode(Request $request) + public function invalidateSession(Request $request) { + $identity = $request->input('identity'); + $token = $request->input('token'); + try { - $clientSessionToken = $this->twoFactorAuth->resendCode($request); - + $ok = TwoFactorAuth::forgetTwoFaSession($token, $identity); + return response()->json([ - 'status' => 'ok', - 'clientToken' => $clientSessionToken, + 'ok' => $ok, ]); } catch (\Exception $e) { - return response()->json([ - 'error' => $e->getMessage(), - ], 400); + return response()->json(['ok' => false]); } } } diff --git a/src/Http/Controllers/Internal/v1/UserController.php b/src/Http/Controllers/Internal/v1/UserController.php index 108b951..9eaf8d6 100644 --- a/src/Http/Controllers/Internal/v1/UserController.php +++ b/src/Http/Controllers/Internal/v1/UserController.php @@ -17,6 +17,7 @@ use Fleetbase\Notifications\UserAcceptedCompanyInvite; use Fleetbase\Notifications\UserInvited; use Fleetbase\Support\NotificationRegistry; +use Fleetbase\Support\TwoFactorAuth; use Fleetbase\Support\Utils; use Illuminate\Http\Request; use Illuminate\Support\Arr; @@ -53,6 +54,43 @@ public function current(Request $request) ); } + /** + * Get the current user's two factor authentication settings. + * + * @return \Illuminate\Http\Response + */ + public function getTwoFactorSettings(Request $request) + { + $user = $request->user(); + + if (!$user) { + return response()->error('No user session found', 401); + } + + $twoFaSettings = TwoFactorAuth::getTwoFaSettingsForUser($user); + + return response()->json($twoFaSettings->value); + } + + /** + * Save the current user's two factor authentication settings. + * + * @return \Illuminate\Http\Response + */ + public function saveTwoFactorSettings(Request $request) + { + $twoFaSettings = $request->array('twoFaSettings'); + $user = $request->user(); + + if (!$user) { + return response()->error('No user session found', 401); + } + + $twoFaSettings = TwoFactorAuth::saveTwoFaSettingsForUser($user, $twoFaSettings); + + return response()->json($twoFaSettings->value); + } + /** * Creates a user, adds the user to company and sends an email to user about being added. * diff --git a/src/Http/Requests/TwoFaValidationRequest.php b/src/Http/Requests/TwoFaValidationRequest.php index bb5da49..944b087 100644 --- a/src/Http/Requests/TwoFaValidationRequest.php +++ b/src/Http/Requests/TwoFaValidationRequest.php @@ -57,7 +57,7 @@ public function messages() 'identity.exists' => 'No user found by this email', 'identity.email' => 'Email used is invalid', 'identity.phone' => 'Phone Number used is invalid', - 'token.required' => 'A two factor session token is required', + 'token.required' => 'A two factor session token is required', ]; } } diff --git a/src/Mail/VerifyEmail.php b/src/Mail/VerifyEmail.php index b544fc7..5a7a030 100644 --- a/src/Mail/VerifyEmail.php +++ b/src/Mail/VerifyEmail.php @@ -4,6 +4,7 @@ use Fleetbase\Models\VerificationCode; use Illuminate\Bus\Queueable; +use Illuminate\Database\Eloquent\Model; use Illuminate\Mail\Mailable; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Queue\SerializesModels; @@ -16,17 +17,59 @@ class VerifyEmail extends Mailable public string $verifyCode; public string $greeting; + public string $messageSubject; + public array $lines = []; /** * Create a new message instance. * * @return void */ - public function __construct($verifyCode, $subject = null, $user = null) + public function __construct($verificationCode, string $subject = null, array $lines = [], Model $user = null) { - $this->verifyCode = $verifyCode instanceof VerificationCode ? $verifyCode->code : $verifyCode; - $this->subject = $subject ?? ($this->verifyCode . ' is your Fleetbase verification code'); - $this->greeting = ($user && isset($user->name)) ? 'Hello, ' . $user->name . '!' : 'Hello!'; + $this->setVerificationCode($verificationCode); + $this->setSubject($subject); + $this->setEmailLines($lines); + $this->setGreeting($user); + } + + public function setVerificationCode($verificationCode): void + { + if ($verificationCode instanceof VerificationCode) { + $this->verifyCode = $verificationCode->code; + } else { + $this->verifyCode = $verificationCode; + } + } + + public function setSubject(?string $subject): void + { + if (is_string($subject) && !empty($subject)) { + $this->messageSubject = $subject; + } else { + $this->messageSubject = $this->verifyCode . ' is your ' . config('app.name') . ' verification code'; + } + } + + public function setEmailLines(array $lines = []): void + { + if (!empty($lines)) { + $this->lines = $lines; + } else { + $this->lines = [ + 'Welcome to ' . config('app.name') . ', use the code below to verify your email address and complete registration to ' . config('app.name') . '.', + new HtmlString('

Your verification code: ' . $this->verifyCode . '


'), + ]; + } + } + + public function setGreeting(Model $user): void + { + if ($user && isset($user->name)) { + $this->greeting = 'Hello, ' . $user->name . '!'; + } else { + $this->greeting = 'Hello!'; + } } /** @@ -37,11 +80,10 @@ public function __construct($verifyCode, $subject = null, $user = null) public function build() { return $this - ->subject($this->subject) + ->subject($this->messageSubject) ->html((new MailMessage()) ->greeting($this->greeting) - ->line('Welcome to Fleetbase, use the code below to verify your email address and complete registration to Fleetbase.') - ->line(new HtmlString('

Your verification code: ' . $this->verifyCode . '


')) + ->lines($this->lines) ->render() ); } diff --git a/src/Models/Setting.php b/src/Models/Setting.php index 20314fd..f87c9c5 100644 --- a/src/Models/Setting.php +++ b/src/Models/Setting.php @@ -2,10 +2,11 @@ namespace Fleetbase\Models; +use Fleetbase\Casts\Json; +use Fleetbase\Support\Utils; use Fleetbase\Traits\Filterable; use Fleetbase\Traits\HasApiModelBehavior; use Fleetbase\Traits\Searchable; -use Fleetbase\Casts\Json; use Illuminate\Database\Eloquent\Model as EloquentModel; class Setting extends EloquentModel @@ -127,7 +128,7 @@ public static function system($key, $defaultValue = null) * * @param string $key */ - public static function configureSystem($key, $value = null) + public static function configureSystem($key, $value = null): ?Setting { return static::configure('system.' . $key, $value); } @@ -137,7 +138,7 @@ public static function configureSystem($key, $value = null) * * @param string $key */ - public static function configure($key, $value = null) + public static function configure($key, $value = null): ?Setting { return static::updateOrCreate( ['key' => $key], @@ -164,6 +165,16 @@ public static function lookup(string $key, $defaultValue = null) return data_get($setting, 'value', $defaultValue); } + /** + * Get a settting record by key. + * + * @return \Fleetbase\Models\Setting|null + */ + public static function getByKey(string $key) + { + return static::where('key', $key)->first(); + } + public static function getBranding() { $brandingSettings = ['id' => 1, 'uuid' => 1, 'icon_url' => null, 'logo_url' => null]; @@ -196,4 +207,14 @@ public static function getBranding() return $brandingSettings; } + + public function getValue(string $key, $defaultValue = null) + { + return data_get($this->value, $key, $defaultValue); + } + + public function getBoolean(string $key) + { + return Utils::castBoolean($this->getValue($key, false)); + } } diff --git a/src/Models/User.php b/src/Models/User.php index 1106ea8..facf51c 100644 --- a/src/Models/User.php +++ b/src/Models/User.php @@ -576,4 +576,16 @@ public function sendInviteFromCompany(Company $company = null): bool return true; } -} \ No newline at end of file + + public function getIdentity(): ?string + { + $email = data_get($this, 'email'); + $phone = data_get($this, 'phone'); + + if ($email) { + return $email; + } + + return $phone; + } +} diff --git a/src/Models/VerificationCode.php b/src/Models/VerificationCode.php index b6df8d1..0294b17 100644 --- a/src/Models/VerificationCode.php +++ b/src/Models/VerificationCode.php @@ -92,17 +92,18 @@ public static function generateFor($subject = null, $for = 'general_verification } /** static method to generate code for email verification */ - public static function generateEmailVerificationFor($subject, $for = 'email_verification', \Closure $messageCallback = null, $meta = [], $expireAfter = null) + public static function generateEmailVerificationFor($subject, $for = 'email_verification', \Closure $messageCallback = null, \Closure $linesCallback = null, $meta = [], $expireAfter = null) { $verifyCode = static::generateFor($subject, $for, false); $verifyCode->expires_at = $expireAfter === null ? Carbon::now()->addHour() : $expireAfter; $verifyCode->meta = $meta; $verifyCode->save(); - $emailSubject = $messageCallback ? $messageCallback($verifyCode) : null; + $emailSubject = is_callable($messageCallback) ? $messageCallback($verifyCode) : null; + $emailLines = is_callable($linesCallback) ? $linesCallback($verifyCode) : []; if (isset($subject->email)) { - Mail::to($subject)->send(new VerifyEmail($verifyCode, $emailSubject, $subject)); + Mail::to($subject)->send(new VerifyEmail($verifyCode, $emailSubject, $emailLines, $subject)); } return $verifyCode; @@ -117,7 +118,7 @@ public static function generateSmsVerificationFor($subject, $for = 'phone_verifi $verifyCode->save(); if ($subject->phone) { - Twilio::message($subject->phone, $messageCallback ? $messageCallback($verifyCode) : "Your Fleetbase verification code is {$verifyCode->code}"); + Twilio::message($subject->phone, $messageCallback ? $messageCallback($verifyCode) : 'Your ' . config('app.name') . ' verification code is ' . $verifyCode->code); } return $verifyCode; diff --git a/src/Notifications/UserForgotPassword.php b/src/Notifications/UserForgotPassword.php index c7d3b7e..166a7a9 100644 --- a/src/Notifications/UserForgotPassword.php +++ b/src/Notifications/UserForgotPassword.php @@ -53,7 +53,7 @@ public function via($notifiable) public function toMail($notifiable) { return (new MailMessage()) - ->subject('Your password reset link for Fleetbase') + ->subject('Your password reset link for ' . config('app.name')) ->greeting('Hello, ' . $notifiable->name) ->line('Looks like you (or someone phishy) has requested to reset your password. If you did not request a password reset link, ignore this email. If you have indeed forgot your password click the button below to reset your password using the code provided below.') ->line(new HtmlString('

Your password reset code: ' . $this->verificationCode->code . '

')) diff --git a/src/Support/TwoFactorAuth.php b/src/Support/TwoFactorAuth.php index 71c128a..2a0afb9 100644 --- a/src/Support/TwoFactorAuth.php +++ b/src/Support/TwoFactorAuth.php @@ -2,124 +2,254 @@ namespace Fleetbase\Support; -use Carbon\Carbon as CarbonDateTime; -use Fleetbase\Models\VerificationCode; -use Fleetbase\Http\Requests\TwoFaValidationRequest; +use Fleetbase\Models\Company; use Fleetbase\Models\Setting; use Fleetbase\Models\User; +use Fleetbase\Models\VerificationCode; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Arr; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Cache; +use Illuminate\Support\HtmlString; use Illuminate\Support\Str; -use Illuminate\Http\Request; -use Illuminate\Support\Carbon; /** - * Class TwoFactorAuth - * - * @package Fleetbase\Support + * Class TwoFactorAuth. */ class TwoFactorAuth { /** - * Save Two-Factor Authentication settings. + * Save Two-Factor Authentication settings for System wide usage. * - * @param \Illuminate\Http\Request $request - * @throws \Exception - * @return array + * @param array $twoFaSettings an array containing Two-Factor Authentication settings + * + * @return \Fleetbase\Models\Setting|null the saved Two-Factor Authentication settings, or null on failure + * + * @throws \Exception if invalid Two-Factor Authentication settings data is provided */ - public static function saveSettings($request) + public static function configureTwoFaSettings(array $twoFaSettings = []): ?Setting { - $twoFaSettings = $request->input('twoFaSettings'); if (!is_array($twoFaSettings)) { throw new \Exception('Invalid 2FA settings data.'); } - Setting::configure('2fa', $twoFaSettings); - return [ - 'status' => 'ok', - 'message' => '2Fa settings successfully saved.', - ]; + return Setting::configureSystem('2fa', $twoFaSettings); } /** - * Get Two-Factor Authentication settings. + * Get system wide Two-Factor Authentication settings. * - * @return array + * @return \Fleetbase\Models\Setting|null the Two-Factor Authentication settings, or null if not found */ - public static function getSettings() + public static function getTwoFaConfiguration(): ?Setting { - $twoFaSettings = Setting::lookup('2fa', ['enabled' => false, 'method' => 'authenticator_app']); + $twoFaSettings = Setting::getByKey('system.2fa'); + + if (!$twoFaSettings) { + $twoFaSettings = static::configureTwoFaSettings(['enabled' => false, 'method' => 'email']); + } return $twoFaSettings; } /** - * Verify Two-Factor Authentication code. + * Save Two-Factor Authentication settings for a specific subject (e.g., User, Company). * - * @param \Fleetbase\Http\Requests\TwoFaValidationRequest $request - * @throws \Exception - * @return array + * @param \Illuminate\Database\Eloquent\Model $subject the subject model for which to save the settings + * @param array $twoFaSettings an array containing Two-Factor Authentication settings + * + * @return \Fleetbase\Models\Setting the saved Two-Factor Authentication settings + * + * @throws \Exception if the subject is not an instance of Model */ - public static function validateSession(TwoFaValidationRequest $request): string + private static function saveTwoFaSettingsForSubject(Model $subject, array $twoFaSettings = []): Setting { - if (!self::isEnabled()) { - throw new \Exception('Two Factor Authentication is not enabled.'); + if (!$subject instanceof Model) { + throw new \Exception('Subject must be a model.'); } - $token = $request->input('token'); - $identity = $request->input('identity'); - $clientToken = $request->input('clientToken'); + $type = Str::singular(Str::snake($subject->getTable(), '-')); // `user` - `company` + $key = $type . '.' . $subject->getKey() . '.2fa'; + + return Setting::configure($key, $twoFaSettings); + } - // IF ALREADY AN ACTIVE CLIENT TOKEN SESSION CHECK FOR VERIFICATION CODE SENT AND JUST RETURN CLIENT TOKEN + /** + * Save Two-Factor Authentication settings for a user. + * + * @param \Fleetbase\Models\User $user the user for which to save the settings + * @param array $twoFaSettings an array containing Two-Factor Authentication settings + * + * @return \Fleetbase\Models\Setting the saved Two-Factor Authentication settings + */ + public static function saveTwoFaSettingsForUser(User $user, array $twoFaSettings = []): Setting + { + return static::saveTwoFaSettingsForSubject($user, $twoFaSettings); + } + + /** + * Save Two-Factor Authentication settings for a company. + * + * @param \Fleetbase\Models\Company $company the company for which to save the settings + * @param array $twoFaSettings an array containing Two-Factor Authentication settings + * + * @return \Fleetbase\Models\Setting the saved Two-Factor Authentication settings + */ + public static function saveTwoFaSettingsForCompany(Company $company, array $twoFaSettings = []): Setting + { + return static::saveTwoFaSettingsForSubject($company, $twoFaSettings); + } + + /** + * Get Two-Factor Authentication settings for a specific subject (e.g., User, Company). + * + * @param \Illuminate\Database\Eloquent\Model $subject the subject model for which to retrieve the settings + * + * @return \Fleetbase\Models\Setting the Two-Factor Authentication settings for the subject + * + * @throws \Exception if the subject is not an instance of Model + */ + private static function getTwoFaSettingsForSubject(Model $subject): Setting + { + if (!$subject instanceof Model) { + throw new \Exception('Subject must be a model.'); + } + + $type = Str::singular(Str::snake($subject->getTable(), '-')); // `user` - `company` + $key = $type . '.' . $subject->getKey() . '.2fa'; + + // Get the settings + $twoFaSettings = Setting::getByKey($key); + + if (!$twoFaSettings) { + $twoFaSettings = static::saveTwoFaSettingsForSubject($subject, ['enabled' => false, 'method' => 'email']); + } + + return $twoFaSettings; + } + + /** + * Get Two-Factor Authentication settings for a user. + * + * @param \Fleetbase\Models\User $user the user for which to retrieve the settings + * + * @return \Fleetbase\Models\Setting the Two-Factor Authentication settings for the user + */ + public static function getTwoFaSettingsForUser(User $user): Setting + { + return static::getTwoFaSettingsForSubject($user); + } + + /** + * Get Two-Factor Authentication settings for a company. + * + * @param \Fleetbase\Models\Company $company the company for which to retrieve the settings + * + * @return \Fleetbase\Models\Setting the Two-Factor Authentication settings for the company + */ + public static function getTwoFaSettingsForCompany(Company $company): Setting + { + return static::getTwoFaSettingsForSubject($company); + } + + /** + * Get a client session token from a Two-Factor Authentication session. + * + * @param string $token the Two-Factor Authentication token + * @param string $identity the user identity + * @param string|null $clientToken the optional client session token + * + * @return string the client session token + * + * @throws \Exception if Two-Factor Authentication is not enabled or the session is invalid + */ + public static function getClientSessionTokenFromTwoFaSession(string $token, string $identity, string $clientToken = null): string + { + // Get user from identity + $user = static::getUserFromIdentity($identity); + if (!$user) { + throw new \Exception('No user found for the identity provided.'); + } + + // Check if enabled 2FA + if (!self::isEnabled($user)) { + throw new \Exception('2FA Authentication is not enabled.'); + } + + // If a client session token is provided validate by fetching the verification code + // If a verification code exists then we just return the current valid client session if ($clientToken) { - $clientTokenDecoded = base64_decode($clientToken); - $clientTokenParts = explode('|', $clientTokenDecoded); - $verificationCodeId = $clientTokenParts[1]; + $verificationCode = static::getVerificationCodeFromClientToken($clientToken); - if ($verificationCodeId) { - $verificationCode = VerificationCode::where('uuid', $verificationCodeId)->exists(); + // If verification code has expired throw exception + if ($verificationCode && $verificationCode->hasExpired()) { + static::forgetTwoFaSession($token, $identity); + throw new \Exception('2FA Verification code has expired.'); + } - if ($verificationCode) { - return $clientToken; - } + if ($verificationCode) { + return $clientToken; + } else { + static::forgetTwoFaSession($token, $identity); + throw new \Exception('2FA Verification code is invalid or has expired.'); } } - $user = User::where(function ($query) use ($identity) { - $query->where('email', $identity)->orWhere('phone', $identity); - })->first(); + // Decrypt two fa session token + $twoFaSessionKey = static::decryptSessionKey($token, $user->uuid); - if ($user) { - $twoFaSessionKey = 'two_fa_session:' . $user->uuid . ':' . $token; + // Validate session key that it is valid and exists + if (static::isTwoFaSessionKeyValid($twoFaSessionKey, $user)) { + // Send the verification code then create a client session for the verification code and user + $verificationCode = static::sendVerificationCode($user); + $clientToken = static::createClientSessionToken($verificationCode); - if (Cache::has($twoFaSessionKey)) { - // create token creating info about the session and send verification code - $expireAfter = Carbon::now()->addSeconds(61); - $verificationCode = static::sendVerificationCode($user, $expireAfter); - $clientSessionToken = base64_encode($expireAfter . '|' . $verificationCode->uuid . '|' . Str::random()); + return $clientToken; + } - return $clientSessionToken; - } + throw new \Exception('2FA Authentication session is invalid'); + } + + /** + * Validate a Two-Factor Authentication session token. + * + * @param string $token the Two-Factor Authentication token + * @param string $identity the user identity + * @param string|null $clientToken the optional client session token + * + * @return bool true if the session token is valid, false otherwise + */ + public static function validateSessionToken(string $token, string $identity, string $clientToken = null): bool + { + try { + return is_string(static::getClientSessionTokenFromTwoFaSession($token, $identity, $clientToken)); + } catch (\Exception $e) { + return false; } - throw new \Exception('Two factor authentication session is invalid'); + return false; } /** - * Send Two-Factor Authentication verification code. + * Send a Two-Factor Authentication verification code to the user. + * + * @param \Fleetbase\Models\User $user the user to send the verification code to + * @param int $expiresAfter the expiration time for the verification code in seconds * - * @param \Fleetbase\Models\User $user - * @param \Carbon\Carbon|null $expireAfter - * @throws \Exception + * @return VerificationCode the generated verification code + * + * @throws \Exception if no phone number or email is available, or an invalid method is selected in settings */ - public static function sendVerificationCode(User $user, ?CarbonDateTime $expireAfter): VerificationCode + public static function sendVerificationCode(User $user, int $expiresAfter = 61): VerificationCode { - $twoFaSettings = Setting::lookup('2fa'); - $method = data_get($twoFaSettings, 'method', 'email'); + $twoFaSettings = static::getTwoFaSettingsForUser($user); + $method = $twoFaSettings->getValue('method', 'email'); + $expiresAfter = Carbon::now()->addSeconds($expiresAfter); - // if no expiration provided default to 1 min - if (!$expireAfter) { - $expireAfter = Carbon::now()->addSeconds(61); - } + // Create SMS and Email message callback + $messageCallback = function ($verificationCode) { + return $verificationCode->code . ' is your ' . config('app.name') . ' 2FA Code'; + }; if ($method === 'sms') { // if user has no phone number throw error @@ -128,7 +258,7 @@ public static function sendVerificationCode(User $user, ?CarbonDateTime $expireA } // create verification code - return VerificationCode::generateSmsVerificationFor($user, '2fa', null, [], $expireAfter); + return VerificationCode::generateSmsVerificationFor($user, '2fa', $messageCallback, [], $expiresAfter); } if ($method === 'email') { @@ -137,185 +267,389 @@ public static function sendVerificationCode(User $user, ?CarbonDateTime $expireA throw new \Exception('No email to send 2FA code to.'); } + // 2FA Message Lines + $linesCallback = function ($verificationCode) { + return [ + new HtmlString('

Your two-factor authentication code is: ' . $verificationCode->code . '

'), + ]; + }; + // create verification code - return VerificationCode::generateEmailVerificationFor($user, '2fa', null, [], $expireAfter); + return VerificationCode::generateEmailVerificationFor($user, '2fa', $messageCallback, $linesCallback, [], $expiresAfter); } + + throw new \Exception('Invalid 2FA method selected in settings.'); } /** - * Check Two-Factor Authentication status for a given user identity. + * Create a Two-Factor Authentication session if enabled. * - * @param \Illuminate\Http\Request $request - * @return array + * @param string $identity the user identity + * + * @return string|null the Two-Factor Authentication session key, or null if not enabled */ - public static function checkTwoFactorStatus(Request $request) + public static function createTwoFaSessionIfEnabled(string $identity): ?string { - $identity = $request->input('identity'); - $isTwoFaEnabled = self::isEnabled(); - $twoFaSession = null; - $isTwoFaValidated = false; - $error = null; + $user = static::getUserFromIdentity($identity); + if (!$user) { + return null; + } - if ($isTwoFaEnabled) { - $twoFaSession = self::start($identity); + $isTwoFaEnabled = self::isEnabled($user); - if ($twoFaSession === null) { - $error = 'No user found using the provided identity'; - } else { - $isTwoFaValidated = self::isTwoFactorSessionValidated($twoFaSession); - } + if ($isTwoFaEnabled) { + return self::start($identity); } - return [ - 'isTwoFaEnabled' => $isTwoFaEnabled, - 'isTwoFaValidated' => $isTwoFaValidated, - 'twoFaSession' => $twoFaSession, - 'error' => $error - ]; + return null; } /** * Check if Two-Factor Authentication is enabled. * - * @return bool + * @return bool true if Two-Factor Authentication is enabled, false otherwise */ - public static function isEnabled() + public static function isEnabled(User $user): bool { - $twoFaSettings = Setting::lookup('2fa'); + $twoFaSettings = static::getTwoFaSettingsForUser($user); + if (!$twoFaSettings) { + return false; + } - return isset($twoFaSettings['enabled']) ? (bool)$twoFaSettings['enabled'] : false; + return $twoFaSettings->getBoolean('enabled'); } /** - * Start the Two-Factor Authentication process and return the session key. + * Start a Two-Factor Authentication session. + * + * @param string $identity the user identity + * @param int $tokenLength the length of the generated token * - * @param string $identity - * @return string|null + * @return string|null the Two-Factor Authentication session key, or null on failure */ - public static function start(string $identity): ?string + public static function start(string $identity, int $tokenLength = 40): ?string { - $twoFaSession = Str::random(40); - - $user = User::where(function ($query) use ($identity) { - $query->where('email', $identity)->orWhere('phone', $identity); - })->first(); + $user = static::getUserFromIdentity($identity); if ($user) { - $twoFaSessionKey = 'two_fa_session:' . $user->uuid . ':' . $twoFaSession; - Cache::put($twoFaSessionKey, $user->uuid, now()->addMinutes(10)); - return $twoFaSession; + $token = Str::random($tokenLength); + $twoFaSessionKey = static::createTwoFaSessionKey($user, $token, true); + + return static::encryptSessionKey($twoFaSessionKey, $user->uuid); } return null; } /** - * Check if the Two-Factor Authentication session is validated. + * Verify a Two-Factor Authentication code and return a user token. * - * @param string|null $twoFaSession - The Two-Factor Authentication session key - * @return bool - True if the session is validated, false otherwise + * @param string $code the user-provided verification code + * @param string $token the Two-Factor Authentication token + * @param string $clientToken the client session token + * + * @return string the user token + * + * @throws \Exception if verification code is invalid or expired, or session is invalid */ - public static function isTwoFactorSessionValidated(?string $twoFaSession = null): bool + public static function verifyCode(string $code, string $token, string $clientToken): string { - if ($twoFaSession === null) { - return false; + // Get verification code from the client token + $verificationCode = static::getVerificationCodeFromClientToken($clientToken); + + // If no verification code return null + if (!$verificationCode) { + throw new \Exception('Verification code is invalid.'); } - // do check here - return false; + + // If we have verification code then get user from it + if ($verificationCode) { + // Get user from verification code + $user = static::getUserFromVerificationCode($verificationCode); + + // If no user found in the verification code + if (!$user) { + throw new \Exception('User not found for verification code.'); + } + + // Get the user identity + $identity = $user->getIdentity(); + + // Next we will validate the session token + if (static::validateSessionToken($token, $identity, $clientToken)) { + // Get the two factor session key + $twoFaSessionKey = static::decryptSessionKey($token, $user->uuid); + + // If session key is valid + if (static::isTwoFaSessionKeyValid($twoFaSessionKey, $user)) { + // Make sure verification code has not expired + if ($verificationCode->hasExpired()) { + throw new \Exception('Verification code has expired.'); + } + + // Check if verification code matches user provided code + $verificationCodeMatches = $verificationCode->code === $code; + if ($verificationCodeMatches) { + // Kill the two fa session + Cache::forget($twoFaSessionKey); + + // Authenticate the user + $token = $user->createToken($user->uuid); + + return $token->plainTextToken; + } + + throw new \Exception('Verification code does not match.'); + } + } + } + + throw new \Exception('Verification code is invalid.'); } /** - * Verify the Two-Factor Authentication code received via SMS. + * Resend a Two-Factor Authentication verification code. + * + * @param string $identity the user identity + * @param string $token the Two-Factor Authentication token + * + * @return string the newly generated client session token * - * @param \Illuminate\Http\Request $request - * @throws \Exception - * @return array + * @throws \Exception if no user found or the Two-Factor Authentication session is invalid */ - public static function verifyCode(Request $request): ?string + public static function resendCode(string $identity, string $token): string { - try { - $token = $request->input('token'); - $identity = $request->input('identity'); - $clientToken = $request->input('clientToken'); - $verificationCode = $request->input('verificationCode'); + $user = static::getUserFromIdentity($identity); + if (!$user) { + throw new \Exception('No user found using the provided identity'); + } - $user = User::where(function ($query) use ($identity) { - $query->where('email', $identity)->orWhere('phone', $identity); - })->first(); + // Make sure two factor session is valid + if (!static::validateSessionToken($token, $identity)) { + throw new \Exception('2FA session is invalid.'); + } - if (!$user) { - throw new \Exception('No user found using the provided identity'); - } + // Send new verification code for user + $verificationCode = static::sendVerificationCode($user); + + // Return with newly generated client session token for the new verification code + return static::createClientSessionToken($verificationCode); + } + + /** + * Create a client session token for a verification code. + * + * @param \Fleetbase\Models\VerificationCode $verificationCode the verification code + * @param int $expiresAfter the expiration time for the client session token in seconds + * + * @return string the client session token + */ + public static function createClientSessionToken(VerificationCode $verificationCode, int $expiresAfter = 61): string + { + $expiresAfter = Carbon::now()->addSeconds($expiresAfter); + $clientToken = base64_encode($expiresAfter . '|' . $verificationCode->uuid . '|' . Str::random()); + + return $clientToken; + } - $twoFaSessionKey = 'two_fa_session:' . $user->uuid . ':' . $token; + /** + * Check if a Two-Factor Authentication session key is valid. + * + * @param string $twoFaSessionKey the Two-Factor Authentication session key + * @param \Fleetbase\Models\User $user the user the session key was created for + * + * @return bool true if the session key is valid, false otherwise + */ + public static function isTwoFaSessionKeyValid(string $twoFaSessionKey, User $user): bool + { + $exists = Cache::has($twoFaSessionKey); - if (Cache::has($twoFaSessionKey)) { - $userInputCode = $request->input('verificationCode'); + if ($exists) { + $parts = explode(':', $twoFaSessionKey); + $userId = Arr::get($parts, 1); - if ($clientToken) { - $clientTokenDecoded = base64_decode($clientToken); - $clientTokenParts = explode('|', $clientTokenDecoded); - $verificationCodeId = $clientTokenParts[1]; + return Str::isUuid($userId) && $userId === $user->uuid; + } - if ($verificationCodeId) { - $verificationCode = VerificationCode::where('uuid', $verificationCodeId)->first(); + return false; + } - if ($verificationCode) { - if ($verificationCode->expires_at < now()) { - throw new \Exception('Verification code has expired. Please request a new one.'); - } + /** + * Forget the Two-Factor Authentication session based on the provided token and identity. + * + * @param string $token the token associated with the Two-Factor Authentication session + * @param string $identity The identity (e.g., username) of the user. + * + * @return bool returns true if the Two-Factor Authentication session was successfully forgotten, + * false otherwise + * + * @throws \Exception thrown when no user is found for the provided identity + */ + public static function forgetTwoFaSession(string $token, string $identity): bool + { + $user = static::getUserFromIdentity($identity); + if (!$user) { + throw new \Exception('No user found for the identity provided.'); + } - $verificationCodeMatches = $verificationCode->code === $userInputCode; + // Get session key and destroy it + $twoFaSessionKey = static::decryptSessionKey($token, $user); - if ($verificationCodeMatches) { - Cache::forget($twoFaSessionKey); + return Cache::forget($twoFaSessionKey); + } - // authenticate the user - $ip = $request->ip(); - $token = $user->createToken($ip); + /** + * Create a Two-Factor Authentication session key. + * + * @param \Fleetbase\Models\User $user the user for whom the session key is created + * @param string $token the Two-Factor Authentication token + * @param bool $storeInCache whether to store the key in the cache + * @param int $expiresAfter the expiration time for the session key in seconds + * + * @return string the Two-Factor Authentication session key + */ + private static function createTwoFaSessionKey(User $user, string $token, bool $storeInCache = false, int $expiresAfter = 600): string + { + $twoFaSessionKey = 'two_fa_session:' . $user->uuid . ':' . $token; - return $token->plainTextToken; - } else { - throw new \Exception('Verification code does not match. User entered: ' . $userInputCode . ', Expected: ' . $verificationCode->code); - } - } - } - } - } + if ($storeInCache) { + Cache::put($twoFaSessionKey, $user->uuid, Carbon::now()->addSeconds($expiresAfter)); + } - throw new \Exception('Invalid verification code'); - } catch (\Exception $e) { - \Illuminate\Support\Facades\Log::error('Error during verification: ' . $e->getMessage()); - throw $e; + return $twoFaSessionKey; + } + + /** + * Get a user based on the provided identity (email or phone). + * + * @param string $identity the user identity (email or phone) + * + * @return \Fleetbase\Models\User|null the user, or null if not found + */ + private static function getUserFromIdentity(string $identity): ?User + { + return User::where(function ($query) use ($identity) { + $query->where('email', $identity)->orWhere('phone', $identity); + })->first(); + } + + /** + * Get a user based on the provided verification code. + * + * @param \Fleetbase\Models\VerificationCode|null $verificationCode the verification code + * + * @return \Fleetbase\Models\User|null the user, or null if not found + */ + private static function getUserFromVerificationCode(VerificationCode $verificationCode = null): ?User + { + if ($verificationCode instanceof VerificationCode) { + $subject = $verificationCode->subject; + + if ($subject instanceof User) { + return $subject; + } } + + return null; } - public static function resendCode(Request $request): string + /** + * Decode a client session token. + * + * @param string $clientToken the client session token + * + * @return array the decoded client session token parts + */ + private static function decodeClientToken(string $clientToken): array { - try { - $identity = $request->input('identity'); - // $twoFaSession = self::start($identity); + $clientTokenDecoded = base64_decode($clientToken); + $clientTokenParts = explode('|', $clientTokenDecoded); - // if ($twoFaSession === null) { - // throw new \Exception('No user found using the provided identity'); - // } + return $clientTokenParts; + } + + /** + * Get a verification code based on the provided client session token. + * + * @param string $clientToken the client session token + * + * @return \Fleetbase\Models\VerificationCode|null the verification code, or null if not found + */ + private static function getVerificationCodeFromClientToken(string $clientToken): ?VerificationCode + { + $clientTokenParts = static::decodeClientToken($clientToken); + $verificationCodeId = $clientTokenParts[1]; - $user = User::where(function ($query) use ($identity) { - $query->where('email', $identity)->orWhere('phone', $identity); - })->first(); + if ($verificationCodeId) { + $verificationCode = VerificationCode::where('uuid', $verificationCodeId)->first(); - if (!$user) { - throw new \Exception('No user found using the provided identity'); + if ($verificationCode) { + return $verificationCode; } + } - $expireAfter = Carbon::now()->addSeconds(61); - $verificationCode = static::sendVerificationCode($user, $expireAfter); - $clientSessionToken = base64_encode($expireAfter . '|' . $verificationCode->uuid . '|' . Str::random()); + return null; + } - return $clientSessionToken; - } catch (\Exception $e) { - \Illuminate\Support\Facades\Log::error('Error during resendCode: ' . $e->getMessage()); - throw $e; + /** + * Encrypts the session key using AES-256-CBC encryption with an initialization vector (IV). + * + * @param mixed $data the data to be encrypted + * @param string $key the encryption key + * + * @return string the base64-encoded result of encrypting the data + */ + private static function encryptSessionKey($data, string $key): ?string + { + // Encrypt the data + $ivLength = openssl_cipher_iv_length('aes-256-cbc'); + if ($ivLength === false) { + return null; + } + $iv = openssl_random_pseudo_bytes($ivLength); + if ($iv === false) { + return null; } + $encrypted = openssl_encrypt(gzcompress($data), 'aes-256-cbc', $key, 0, $iv); + if ($encrypted === false) { + return null; + } + + // Combine IV and encrypted data + $result = $iv . $encrypted; + + return base64_encode($result); + } + + /** + * Decrypts the encrypted session key using AES-256-CBC decryption with an initialization vector (IV). + * + * @param string $encrypted the base64-encoded encrypted data + * @param string $key the decryption key + * + * @return mixed the decrypted and decompressed original data + */ + private static function decryptSessionKey(string $encrypted, string $key): ?string + { + // Decode from base64 + $data = base64_decode($encrypted); + + // Extract IV and encrypted data + $ivLength = openssl_cipher_iv_length('aes-256-cbc'); + $iv = substr($data, 0, $ivLength); + $encryptedData = substr($data, $ivLength); + + // Decrypt and decompress + $decrypted = openssl_decrypt($encryptedData, 'aes-256-cbc', $key, 0, $iv); + if ($decrypted === false) { + return null; + } + + $decompressed = gzuncompress($decrypted); + if ($decompressed === false) { + return null; + } + + return $decompressed; } } diff --git a/src/routes.php b/src/routes.php index 7fa5d5e..6251a2b 100644 --- a/src/routes.php +++ b/src/routes.php @@ -87,6 +87,12 @@ function ($router) { $router->post('accept-company-invite', 'UserController@acceptCompanyInvite'); } ); + $router->group( + ['prefix' => 'companies'], + function ($router) { + $router->get('find/{id}', 'CompanyController@findCompany'); + } + ); $router->group( ['prefix' => 'settings'], function ($router) { @@ -97,9 +103,10 @@ function ($router) { ['prefix' => 'two-fa'], function ($router) { $router->get('check', 'TwoFaController@checkTwoFactor'); - $router->post('validate-session', 'TwoFaController@validateSession'); - $router->post('verify-code', 'TwoFaController@verifyCode'); - $router->post('resend-code', 'TwoFaController@resendCode'); + $router->post('validate', 'TwoFaController@validateSession'); + $router->post('verify', 'TwoFaController@verifyCode'); + $router->post('resend', 'TwoFaController@resendCode'); + $router->post('invalidate', 'TwoFaController@invalidateSession'); } ); $router->group( @@ -148,8 +155,8 @@ function ($router, $controller) { $router->fleetbaseRoutes( 'two-fa', function ($router, $controller) { - $router->post('settings', $controller('saveSettings')); - $router->get('settings', $controller('getSettings')); + $router->post('config', $controller('saveSystemConfig')); + $router->get('config', $controller('getSystemConfig')); } ); $router->fleetbaseRoutes('api-events'); @@ -162,7 +169,10 @@ function ($router, $controller) { } ); $router->fleetbaseRoutes('webhook-request-logs'); - $router->fleetbaseRoutes('companies'); + $router->fleetbaseRoutes('companies', function ($router, $controller) { + $router->post('two-fa', $controller('saveTwoFactorSettings')); + $router->get('two-fa', $controller('getTwoFactorSettings')); + }); $router->fleetbaseRoutes( 'users', function ($router, $controller) { @@ -174,6 +184,8 @@ function ($router, $controller) { $router->delete('bulk-delete', $controller('bulkDelete')); $router->post('resend-invite', $controller('resendInvitation')); $router->post('set-password', $controller('setCurrentUserPassword')); + $router->post('two-fa', $controller('saveTwoFactorSettings')); + $router->get('two-fa', $controller('getTwoFactorSettings')); } ); $router->fleetbaseRoutes('user-devices'); From 96cafb41a6ba130219e99d7499c5fcdf7c7812bd Mon Sep 17 00:00:00 2001 From: TemuulenBM Date: Wed, 17 Jan 2024 19:20:15 +0800 Subject: [PATCH 15/22] Completed change password flow Completed enforce 2fa action for all users of company --- .../Internal/v1/CompanyController.php | 15 +++--- .../Internal/v1/TwoFaController.php | 1 + .../Internal/v1/UserController.php | 46 +++++++++++++++++++ .../Internal/ValidatePasswordRequest.php | 45 ++++++++++++++++++ src/Models/User.php | 5 ++ src/Support/TwoFactorAuth.php | 10 ++++ src/routes.php | 4 +- 7 files changed, 116 insertions(+), 10 deletions(-) create mode 100644 src/Http/Requests/Internal/ValidatePasswordRequest.php diff --git a/src/Http/Controllers/Internal/v1/CompanyController.php b/src/Http/Controllers/Internal/v1/CompanyController.php index 8a8311a..654353f 100644 --- a/src/Http/Controllers/Internal/v1/CompanyController.php +++ b/src/Http/Controllers/Internal/v1/CompanyController.php @@ -61,22 +61,19 @@ public function getTwoFactorSettings() return response()->json($twoFaSettings->value); } - /** - * Save the current user's two factor authentication settings. - * - * @return \Illuminate\Http\Response - */ - public function saveTwoFactorSettings(Request $request) + public function enforceTwoFactorSettings(Request $request) { + $twoFaSettings = $request->array('twoFaSettings'); - $company = Auth::getCompany(); + $company = Auth::getCompany(); if (!$company) { return response()->error('No company session found', 401); } - $twoFaSettings = TwoFactorAuth::saveTwoFaSettingsForCompany($company, $twoFaSettings); + TwoFactorAuth::enforceTwoFaForCompanyUsers($company, $twoFaSettings); + TwoFactorAuth::saveTwoFaSettingsForCompany($company, $twoFaSettings); - return response()->json($twoFaSettings->value); + return response()->json(['message' => 'Two-Factor Authentication enforced for all company users']); } } diff --git a/src/Http/Controllers/Internal/v1/TwoFaController.php b/src/Http/Controllers/Internal/v1/TwoFaController.php index 14b78d0..98bfd9e 100644 --- a/src/Http/Controllers/Internal/v1/TwoFaController.php +++ b/src/Http/Controllers/Internal/v1/TwoFaController.php @@ -4,6 +4,7 @@ use Fleetbase\Http\Controllers\Controller; use Fleetbase\Http\Requests\TwoFaValidationRequest; +use Fleetbase\Models\Company; use Fleetbase\Support\TwoFactorAuth; use Illuminate\Http\Request; use Illuminate\Support\Str; diff --git a/src/Http/Controllers/Internal/v1/UserController.php b/src/Http/Controllers/Internal/v1/UserController.php index 9eaf8d6..c76cbf1 100644 --- a/src/Http/Controllers/Internal/v1/UserController.php +++ b/src/Http/Controllers/Internal/v1/UserController.php @@ -10,6 +10,7 @@ use Fleetbase\Http\Requests\Internal\InviteUserRequest; use Fleetbase\Http\Requests\Internal\ResendUserInvite; use Fleetbase\Http\Requests\Internal\UpdatePasswordRequest; +use Fleetbase\Http\Requests\Internal\ValidatePasswordRequest; use Fleetbase\Models\Company; use Fleetbase\Models\CompanyUser; use Fleetbase\Models\Invite; @@ -22,6 +23,7 @@ use Illuminate\Http\Request; use Illuminate\Support\Arr; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; use Maatwebsite\Excel\Facades\Excel; @@ -405,4 +407,48 @@ public static function getWithDriver($id, Request $request) return response()->json(['user' => $user]); } + + /** + * Validate the user's current password. + * + * @param \Fleetbase\Http\Requests\Internal\ValidatePasswordRequest $request + * @return \Illuminate\Http\Response + */ + public function validatePassword(ValidatePasswordRequest $request) + { + $user = $request->user(); + $currentPassword = $request->input('current_password'); + $confirmPassword = $request->input('confirm_password'); + + if (!$user || !$user->checkPassword($currentPassword)) { + return response()->error('Invalid current password', 422); + } + + if ($currentPassword !== $confirmPassword) { + return response()->error('Password is not matching'); + } + + return response()->json(['status' => 'ok']); + } + + /** + * Change the user's password. + * + * @param \Fleetbase\Http\Requests\Internal\UpdatePasswordRequest $request + * @return \Illuminate\Http\Response + */ + public function changeUserPassword(UpdatePasswordRequest $request) + { + $user = $request->user(); + $newPassword = $request->input('password'); + $newConfirmPassword = $request->input('password_confirmation'); + + if ($newPassword !== $newConfirmPassword) { + return response()->error('Password is not matching'); + } + + $user->changePassword($newPassword); + + return response()->json(['status' => 'ok']); + } } diff --git a/src/Http/Requests/Internal/ValidatePasswordRequest.php b/src/Http/Requests/Internal/ValidatePasswordRequest.php new file mode 100644 index 0000000..54d8652 --- /dev/null +++ b/src/Http/Requests/Internal/ValidatePasswordRequest.php @@ -0,0 +1,45 @@ + 'required|string|min:6', + 'confirm_password' => 'required|string|min:6|same:current_password', + ]; + } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + public function messages() + { + return [ + 'current_password.required' => 'The current password is required.', + 'current_password.string' => 'The current password must be a string.', + 'current_password.min' => 'The current password must be at least 8 characters.', + ]; + } +} diff --git a/src/Models/User.php b/src/Models/User.php index facf51c..0ded10e 100644 --- a/src/Models/User.php +++ b/src/Models/User.php @@ -425,6 +425,11 @@ public function changePassword($newPassword): User return $this; } + public function checkPassword(string $password): bool + { + return Hash::check($password, $this->password); + } + /** * Deactivate this user. */ diff --git a/src/Support/TwoFactorAuth.php b/src/Support/TwoFactorAuth.php index 2a0afb9..d01dd94 100644 --- a/src/Support/TwoFactorAuth.php +++ b/src/Support/TwoFactorAuth.php @@ -100,6 +100,16 @@ public static function saveTwoFaSettingsForCompany(Company $company, array $twoF return static::saveTwoFaSettingsForSubject($company, $twoFaSettings); } + + public static function enforceTwoFaForCompanyUsers(Company $company, array $twoFaSettings = []): Setting + { + $users = $company->users; + + foreach ($users as $user) { + return static::saveTwoFaSettingsForSubject($user, $twoFaSettings); + } + } + /** * Get Two-Factor Authentication settings for a specific subject (e.g., User, Company). * diff --git a/src/routes.php b/src/routes.php index 6251a2b..eb29083 100644 --- a/src/routes.php +++ b/src/routes.php @@ -170,7 +170,7 @@ function ($router, $controller) { ); $router->fleetbaseRoutes('webhook-request-logs'); $router->fleetbaseRoutes('companies', function ($router, $controller) { - $router->post('two-fa', $controller('saveTwoFactorSettings')); + $router->post('enforce', $controller('enforceTwoFactorSettings')); $router->get('two-fa', $controller('getTwoFactorSettings')); }); $router->fleetbaseRoutes( @@ -184,6 +184,8 @@ function ($router, $controller) { $router->delete('bulk-delete', $controller('bulkDelete')); $router->post('resend-invite', $controller('resendInvitation')); $router->post('set-password', $controller('setCurrentUserPassword')); + $router->post('validate-password', $controller('validatePassword')); + $router->post('change-password', $controller('changeUserPassword')); $router->post('two-fa', $controller('saveTwoFactorSettings')); $router->get('two-fa', $controller('getTwoFactorSettings')); } From 386cfc8c5ca3c6dffa89f2b666980c0f3d4702d8 Mon Sep 17 00:00:00 2001 From: TemuulenBM Date: Fri, 19 Jan 2024 19:26:39 +0800 Subject: [PATCH 16/22] final version of 2fa --- .../Internal/v1/CompanyController.php | 18 ++++-- .../Internal/v1/InstallerController.php | 2 + .../Internal/v1/TwoFaController.php | 13 +++++ src/Support/TwoFactorAuth.php | 56 ++++++++++++++----- src/routes.php | 3 +- 5 files changed, 71 insertions(+), 21 deletions(-) diff --git a/src/Http/Controllers/Internal/v1/CompanyController.php b/src/Http/Controllers/Internal/v1/CompanyController.php index 654353f..a91b3f7 100644 --- a/src/Http/Controllers/Internal/v1/CompanyController.php +++ b/src/Http/Controllers/Internal/v1/CompanyController.php @@ -61,19 +61,27 @@ public function getTwoFactorSettings() return response()->json($twoFaSettings->value); } - public function enforceTwoFactorSettings(Request $request) - { + /** + * Save the two factor authentication settings for the current company. + * + * @param \Illuminate\Http\Request $request The HTTP request. + * + * @return \Illuminate\Http\Response + */ + public function saveTwoFactorSettings(Request $request) + { $twoFaSettings = $request->array('twoFaSettings'); $company = Auth::getCompany(); if (!$company) { return response()->error('No company session found', 401); } - - TwoFactorAuth::enforceTwoFaForCompanyUsers($company, $twoFaSettings); + if (isset($twoFaSettings['enabled']) && $twoFaSettings['enabled'] === false) { + $twoFaSettings['enforced'] = false; + } TwoFactorAuth::saveTwoFaSettingsForCompany($company, $twoFaSettings); - return response()->json(['message' => 'Two-Factor Authentication enforced for all company users']); + return response()->json(['message' => 'Two-Factor Authentication saved successfully']); } } diff --git a/src/Http/Controllers/Internal/v1/InstallerController.php b/src/Http/Controllers/Internal/v1/InstallerController.php index 08c493c..a3386f0 100644 --- a/src/Http/Controllers/Internal/v1/InstallerController.php +++ b/src/Http/Controllers/Internal/v1/InstallerController.php @@ -2,8 +2,10 @@ namespace Fleetbase\Http\Controllers\Internal\v1; +use App\Models\User; use Fleetbase\Http\Controllers\Controller; use Fleetbase\Models\Setting; +use Fleetbase\Support\TwoFactorAuth; use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; diff --git a/src/Http/Controllers/Internal/v1/TwoFaController.php b/src/Http/Controllers/Internal/v1/TwoFaController.php index 98bfd9e..41f262c 100644 --- a/src/Http/Controllers/Internal/v1/TwoFaController.php +++ b/src/Http/Controllers/Internal/v1/TwoFaController.php @@ -22,6 +22,9 @@ class TwoFaController extends Controller public function saveSystemConfig(Request $request) { $twoFaSettings = $request->array('twoFaSettings'); + if (isset($twoFaSettings['enabled']) && $twoFaSettings['enabled'] === false) { + $twoFaSettings['enforced'] = false; + } $settings = TwoFactorAuth::configureTwoFaSettings($twoFaSettings); return response()->json($settings->value); @@ -150,4 +153,14 @@ public function invalidateSession(Request $request) return response()->json(['ok' => false]); } } + + public function shouldEnforce(Request $request) + { + $user = $request->user(); + $enforceTwoFa = TwoFactorAuth::shouldEnforce($user); + + return response()->json([ + 'shouldEnforce' => $enforceTwoFa, + ]); + } } diff --git a/src/Support/TwoFactorAuth.php b/src/Support/TwoFactorAuth.php index d01dd94..8aba22b 100644 --- a/src/Support/TwoFactorAuth.php +++ b/src/Support/TwoFactorAuth.php @@ -12,6 +12,7 @@ use Illuminate\Support\Facades\Cache; use Illuminate\Support\HtmlString; use Illuminate\Support\Str; +use Illuminate\Support\Facades\Redis; /** * Class TwoFactorAuth. @@ -46,7 +47,7 @@ public static function getTwoFaConfiguration(): ?Setting $twoFaSettings = Setting::getByKey('system.2fa'); if (!$twoFaSettings) { - $twoFaSettings = static::configureTwoFaSettings(['enabled' => false, 'method' => 'email']); + $twoFaSettings = static::configureTwoFaSettings(['enabled' => false, 'method' => 'email', 'enforced' => false]); } return $twoFaSettings; @@ -100,16 +101,6 @@ public static function saveTwoFaSettingsForCompany(Company $company, array $twoF return static::saveTwoFaSettingsForSubject($company, $twoFaSettings); } - - public static function enforceTwoFaForCompanyUsers(Company $company, array $twoFaSettings = []): Setting - { - $users = $company->users; - - foreach ($users as $user) { - return static::saveTwoFaSettingsForSubject($user, $twoFaSettings); - } - } - /** * Get Two-Factor Authentication settings for a specific subject (e.g., User, Company). * @@ -329,6 +320,41 @@ public static function isEnabled(User $user): bool return $twoFaSettings->getBoolean('enabled'); } + public static function shouldEnforce(User $user): bool + { + $systemEnforced = static::isSystemEnforced(); + $companyEnforced = static::isCompanyEnforced($user->company); + $userEnabled = static::isEnabled($user); + + return $userEnabled ? !$userEnabled : $systemEnforced || $companyEnforced; + } + + /** + * Check if Two-Factor Authentication is enforced for company. + * + * @return bool true if Two-Factor Authentication is enforced, false otherwise + */ + public static function isCompanyEnforced(Company $company): bool + { + $twoFaSettings = static::getTwoFaSettingsForCompany($company); + + if ($twoFaSettings) { + return $twoFaSettings->getBoolean('enforced'); + } + return false; + } + + public static function isSystemEnforced(): bool + { + $twoFaSettings = static::getTwoFaConfiguration(); + + if (!$twoFaSettings) { + return false; + } + + return $twoFaSettings->getBoolean('enforced'); + } + /** * Start a Two-Factor Authentication session. * @@ -401,7 +427,7 @@ public static function verifyCode(string $code, string $token, string $clientTok $verificationCodeMatches = $verificationCode->code === $code; if ($verificationCodeMatches) { // Kill the two fa session - Cache::forget($twoFaSessionKey); + Redis::del($twoFaSessionKey); // Authenticate the user $token = $user->createToken($user->uuid); @@ -472,7 +498,7 @@ public static function createClientSessionToken(VerificationCode $verificationCo */ public static function isTwoFaSessionKeyValid(string $twoFaSessionKey, User $user): bool { - $exists = Cache::has($twoFaSessionKey); + $exists = Redis::exists($twoFaSessionKey); if ($exists) { $parts = explode(':', $twoFaSessionKey); @@ -505,7 +531,7 @@ public static function forgetTwoFaSession(string $token, string $identity): bool // Get session key and destroy it $twoFaSessionKey = static::decryptSessionKey($token, $user); - return Cache::forget($twoFaSessionKey); + return Redis::del($twoFaSessionKey); } /** @@ -523,7 +549,7 @@ private static function createTwoFaSessionKey(User $user, string $token, bool $s $twoFaSessionKey = 'two_fa_session:' . $user->uuid . ':' . $token; if ($storeInCache) { - Cache::put($twoFaSessionKey, $user->uuid, Carbon::now()->addSeconds($expiresAfter)); + Redis::set($twoFaSessionKey, $user->uuid, Carbon::now()->addSeconds($expiresAfter)); } return $twoFaSessionKey; diff --git a/src/routes.php b/src/routes.php index eb29083..f138df3 100644 --- a/src/routes.php +++ b/src/routes.php @@ -157,6 +157,7 @@ function ($router, $controller) { function ($router, $controller) { $router->post('config', $controller('saveSystemConfig')); $router->get('config', $controller('getSystemConfig')); + $router->get('enforce', $controller('shouldEnforce')); } ); $router->fleetbaseRoutes('api-events'); @@ -170,8 +171,8 @@ function ($router, $controller) { ); $router->fleetbaseRoutes('webhook-request-logs'); $router->fleetbaseRoutes('companies', function ($router, $controller) { - $router->post('enforce', $controller('enforceTwoFactorSettings')); $router->get('two-fa', $controller('getTwoFactorSettings')); + $router->post('two-fa', $controller('saveTwoFactorSettings')); }); $router->fleetbaseRoutes( 'users', From 3d811c4c7f321fce248dee432dbbbac348846652 Mon Sep 17 00:00:00 2001 From: TemuulenBM Date: Mon, 22 Jan 2024 13:48:05 +0800 Subject: [PATCH 17/22] completed redis set --- src/Support/TwoFactorAuth.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Support/TwoFactorAuth.php b/src/Support/TwoFactorAuth.php index 8aba22b..c7ece2b 100644 --- a/src/Support/TwoFactorAuth.php +++ b/src/Support/TwoFactorAuth.php @@ -9,7 +9,6 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Arr; use Illuminate\Support\Carbon; -use Illuminate\Support\Facades\Cache; use Illuminate\Support\HtmlString; use Illuminate\Support\Str; use Illuminate\Support\Facades\Redis; @@ -549,7 +548,8 @@ private static function createTwoFaSessionKey(User $user, string $token, bool $s $twoFaSessionKey = 'two_fa_session:' . $user->uuid . ':' . $token; if ($storeInCache) { - Redis::set($twoFaSessionKey, $user->uuid, Carbon::now()->addSeconds($expiresAfter)); + $expirationTime = Carbon::now()->addSeconds($expiresAfter)->timestamp; + Redis::set($twoFaSessionKey, $user->uuid, 'EX', $expirationTime); } return $twoFaSessionKey; From c9f456483440de9d684209e716cd6ac064244104 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Mon, 22 Jan 2024 14:59:13 +0800 Subject: [PATCH 18/22] preparing for next release --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 3ef69ef..1130050 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "fleetbase/core-api", - "version": "1.3.11", + "version": "1.3.12", "description": "Core Framework and Resources for Fleetbase API", "keywords": [ "fleetbase", From 32bd87c4b4fe0d45461339b7878a064273fb245c Mon Sep 17 00:00:00 2001 From: TemuulenBM Date: Mon, 22 Jan 2024 15:20:53 +0800 Subject: [PATCH 19/22] Minor patches --- src/Http/Controllers/Internal/v1/AuthController.php | 6 +++--- src/Http/Controllers/Internal/v1/InstallerController.php | 2 -- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Http/Controllers/Internal/v1/AuthController.php b/src/Http/Controllers/Internal/v1/AuthController.php index 3141851..e3682be 100644 --- a/src/Http/Controllers/Internal/v1/AuthController.php +++ b/src/Http/Controllers/Internal/v1/AuthController.php @@ -56,9 +56,6 @@ public function login(LoginRequest $request) return response()->error('No user found by the provided identity.', 401, ['code' => 'no_user']); } - // Create token - $token = $user->createToken($user->uuid); - // Check if 2FA enabled if (TwoFactorAuth::isEnabled($user)) { $twoFaSession = TwoFactorAuth::start($user); @@ -69,6 +66,9 @@ public function login(LoginRequest $request) ]); } + // Create token + $token = $user->createToken($user->uuid); + return response()->json(['token' => $token->plainTextToken]); if (Auth::isInvalidPassword($password, $user->password)) { diff --git a/src/Http/Controllers/Internal/v1/InstallerController.php b/src/Http/Controllers/Internal/v1/InstallerController.php index a3386f0..08c493c 100644 --- a/src/Http/Controllers/Internal/v1/InstallerController.php +++ b/src/Http/Controllers/Internal/v1/InstallerController.php @@ -2,10 +2,8 @@ namespace Fleetbase\Http\Controllers\Internal\v1; -use App\Models\User; use Fleetbase\Http\Controllers\Controller; use Fleetbase\Models\Setting; -use Fleetbase\Support\TwoFactorAuth; use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; From 65a2caf0b755c7c81b4c38d41d27057d72f1fae0 Mon Sep 17 00:00:00 2001 From: TemuulenBM Date: Mon, 22 Jan 2024 16:16:21 +0800 Subject: [PATCH 20/22] corrected ValidatePasswordRequest --- .../Internal/v1/UserController.php | 12 ------ .../Internal/ValidatePasswordRequest.php | 42 ++++++++++++++++--- src/Http/Requests/OnboardRequest.php | 2 +- 3 files changed, 38 insertions(+), 18 deletions(-) diff --git a/src/Http/Controllers/Internal/v1/UserController.php b/src/Http/Controllers/Internal/v1/UserController.php index c76cbf1..0b24f8e 100644 --- a/src/Http/Controllers/Internal/v1/UserController.php +++ b/src/Http/Controllers/Internal/v1/UserController.php @@ -416,18 +416,6 @@ public static function getWithDriver($id, Request $request) */ public function validatePassword(ValidatePasswordRequest $request) { - $user = $request->user(); - $currentPassword = $request->input('current_password'); - $confirmPassword = $request->input('confirm_password'); - - if (!$user || !$user->checkPassword($currentPassword)) { - return response()->error('Invalid current password', 422); - } - - if ($currentPassword !== $confirmPassword) { - return response()->error('Password is not matching'); - } - return response()->json(['status' => 'ok']); } diff --git a/src/Http/Requests/Internal/ValidatePasswordRequest.php b/src/Http/Requests/Internal/ValidatePasswordRequest.php index 54d8652..4f6a5b2 100644 --- a/src/Http/Requests/Internal/ValidatePasswordRequest.php +++ b/src/Http/Requests/Internal/ValidatePasswordRequest.php @@ -3,6 +3,38 @@ namespace Fleetbase\Http\Requests\Internal; use Fleetbase\Http\Requests\FleetbaseRequest; +use Illuminate\Contracts\Validation\Rule; + +class ConfirmCurrentPassword implements Rule { + protected $user; + + public function __construct($user) + { + $this->user = $user; + } + + /** + * Determine if the validation rule passes. + * + * @param string $attribute + * + * @return bool + */ + public function passes($attribute, $value) + { + return $this->user && $this->user->checkPassword($value); + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return 'The current password provided is invalid.'; + } +} class ValidatePasswordRequest extends FleetbaseRequest { @@ -24,8 +56,8 @@ public function authorize() public function rules() { return [ - 'current_password' => 'required|string|min:6', - 'confirm_password' => 'required|string|min:6|same:current_password', + 'password' => ['required', 'string', 'min:4', 'confirmed', new ConfirmCurrentPassword($this->user())], + 'password_confirmation' => ['required', 'string'], ]; } @@ -37,9 +69,9 @@ public function rules() public function messages() { return [ - 'current_password.required' => 'The current password is required.', - 'current_password.string' => 'The current password must be a string.', - 'current_password.min' => 'The current password must be at least 8 characters.', + 'password.required' => 'The current password is required.', + 'password.string' => 'The current password must be a string.', + 'password.min' => 'The current password must be at least 8 characters.', ]; } } diff --git a/src/Http/Requests/OnboardRequest.php b/src/Http/Requests/OnboardRequest.php index 5bf738d..3515441 100644 --- a/src/Http/Requests/OnboardRequest.php +++ b/src/Http/Requests/OnboardRequest.php @@ -27,7 +27,7 @@ public function rules() 'name' => ['required'], 'email' => ['required', 'email', Rule::unique('users', 'email')->whereNull('deleted_at')], 'phone' => ['nullable', Rule::unique('users', 'phone')->whereNull('deleted_at')], - 'password' => ['required', 'confirmed'], + 'password' => ['required', 'confirmed', 'min:4'], 'password_confirmation' => ['required'], 'organization_name' => ['required'], ]; From 47bc48cb88b79265d5ac896a676d6366f3feaf0b Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Mon, 22 Jan 2024 16:59:44 +0800 Subject: [PATCH 21/22] Critical patch for loign and password verification --- src/Http/Controllers/Internal/v1/AuthController.php | 6 +----- src/Http/Controllers/Internal/v1/UserController.php | 1 - src/Models/User.php | 7 +++++++ 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Http/Controllers/Internal/v1/AuthController.php b/src/Http/Controllers/Internal/v1/AuthController.php index e3682be..752d608 100644 --- a/src/Http/Controllers/Internal/v1/AuthController.php +++ b/src/Http/Controllers/Internal/v1/AuthController.php @@ -66,11 +66,6 @@ public function login(LoginRequest $request) ]); } - // Create token - $token = $user->createToken($user->uuid); - - return response()->json(['token' => $token->plainTextToken]); - if (Auth::isInvalidPassword($password, $user->password)) { return response()->error('Authentication failed using password provided.', 401, ['code' => 'invalid_password']); } @@ -79,6 +74,7 @@ public function login(LoginRequest $request) return response()->error('User is not verified.', 400, ['code' => 'not_verified']); } + // Login $user->updateLastLogin(); $token = $user->createToken($user->uuid); diff --git a/src/Http/Controllers/Internal/v1/UserController.php b/src/Http/Controllers/Internal/v1/UserController.php index 0b24f8e..b08291c 100644 --- a/src/Http/Controllers/Internal/v1/UserController.php +++ b/src/Http/Controllers/Internal/v1/UserController.php @@ -23,7 +23,6 @@ use Illuminate\Http\Request; use Illuminate\Support\Arr; use Illuminate\Support\Carbon; -use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; use Maatwebsite\Excel\Facades\Excel; diff --git a/src/Models/User.php b/src/Models/User.php index e41bd46..74f0733 100644 --- a/src/Models/User.php +++ b/src/Models/User.php @@ -4,6 +4,7 @@ use Fleetbase\Casts\Json; use Fleetbase\Notifications\UserInvited; +use Fleetbase\Support\Auth; use Fleetbase\Support\Utils; use Fleetbase\Traits\Expandable; use Fleetbase\Traits\Filterable; @@ -423,6 +424,12 @@ public function changePassword($newPassword): User return $this; } + /** + * Checks if password provided is the correct and current password for the user + * + * @param string $password + * @return boolean + */ public function checkPassword(string $password): bool { return Hash::check($password, $this->password); From ab51de26efafc4fd4800d2efc4ad31be4a9147fd Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Wed, 24 Jan 2024 15:55:50 +0800 Subject: [PATCH 22/22] Patches for change password requests, country type, and other critical fixes for core functionality --- .../Internal/v1/AuthController.php | 2 +- .../Internal/v1/CompanyController.php | 5 +-- .../Internal/v1/OnboardController.php | 22 ++-------- .../Internal/v1/TwoFaController.php | 3 +- .../Internal/v1/UserController.php | 40 +++++++++++++++++-- .../Internal/ValidatePasswordRequest.php | 5 ++- src/Http/Requests/LoginRequest.php | 2 +- src/Listeners/HandleAccountCreated.php | 38 ++++++++++++++++++ src/Models/User.php | 8 +--- src/Providers/EventServiceProvider.php | 1 + src/Support/TwoFactorAuth.php | 7 ++-- src/Types/Country.php | 18 ++++++--- src/routes.php | 2 + 13 files changed, 108 insertions(+), 45 deletions(-) create mode 100644 src/Listeners/HandleAccountCreated.php diff --git a/src/Http/Controllers/Internal/v1/AuthController.php b/src/Http/Controllers/Internal/v1/AuthController.php index 752d608..e574058 100644 --- a/src/Http/Controllers/Internal/v1/AuthController.php +++ b/src/Http/Controllers/Internal/v1/AuthController.php @@ -445,7 +445,7 @@ public function signUp(SignUpRequest $request) $companyDetails = $request->input('company'); $newUser = Auth::register($userDetails, $companyDetails); - $token = $newUser->createToken($newUser->uuid); + $token = $newUser->createToken($newUser->uuid); return response()->json(['token' => $token->plainTextToken]); } diff --git a/src/Http/Controllers/Internal/v1/CompanyController.php b/src/Http/Controllers/Internal/v1/CompanyController.php index a91b3f7..e37f0ed 100644 --- a/src/Http/Controllers/Internal/v1/CompanyController.php +++ b/src/Http/Controllers/Internal/v1/CompanyController.php @@ -61,18 +61,17 @@ public function getTwoFactorSettings() return response()->json($twoFaSettings->value); } - /** * Save the two factor authentication settings for the current company. * - * @param \Illuminate\Http\Request $request The HTTP request. + * @param \Illuminate\Http\Request $request the HTTP request * * @return \Illuminate\Http\Response */ public function saveTwoFactorSettings(Request $request) { $twoFaSettings = $request->array('twoFaSettings'); - $company = Auth::getCompany(); + $company = Auth::getCompany(); if (!$company) { return response()->error('No company session found', 401); diff --git a/src/Http/Controllers/Internal/v1/OnboardController.php b/src/Http/Controllers/Internal/v1/OnboardController.php index a6e1292..90358e2 100644 --- a/src/Http/Controllers/Internal/v1/OnboardController.php +++ b/src/Http/Controllers/Internal/v1/OnboardController.php @@ -74,20 +74,6 @@ public function createAccount(OnboardRequest $request) 'status' => 'active', ]); - // create verification code - try { - VerificationCode::generateEmailVerificationFor($user); - } catch (\Throwable $e) { - // If phone number is supplied send via SMS - if ($user->phone) { - try { - VerificationCode::generateSmsVerificationFor($user); - } catch (\Throwable $e) { - // silence - } - } - } - // send account created event event(new AccountCreated($user, $company)); @@ -111,9 +97,9 @@ public function createAccount(OnboardRequest $request) */ public function sendVerificationEmail(Request $request) { - $id = $request->input('session'); + $id = $request->input('session'); $email = $request->input('email'); - $decodedId = base64_decode($id); + $decodedId = base64_decode($id); // Get user using id $user = User::where('uuid', $decodedId)->first(); @@ -142,9 +128,9 @@ public function sendVerificationEmail(Request $request) */ public function sendVerificationSms(Request $request) { - $id = $request->input('session'); + $id = $request->input('session'); $phone = $request->input('phone'); - $decodedId = base64_decode($id); + $decodedId = base64_decode($id); // Get user using id $user = User::where('uuid', $decodedId)->first(); diff --git a/src/Http/Controllers/Internal/v1/TwoFaController.php b/src/Http/Controllers/Internal/v1/TwoFaController.php index 41f262c..e2647bc 100644 --- a/src/Http/Controllers/Internal/v1/TwoFaController.php +++ b/src/Http/Controllers/Internal/v1/TwoFaController.php @@ -4,7 +4,6 @@ use Fleetbase\Http\Controllers\Controller; use Fleetbase\Http\Requests\TwoFaValidationRequest; -use Fleetbase\Models\Company; use Fleetbase\Support\TwoFactorAuth; use Illuminate\Http\Request; use Illuminate\Support\Str; @@ -156,7 +155,7 @@ public function invalidateSession(Request $request) public function shouldEnforce(Request $request) { - $user = $request->user(); + $user = $request->user(); $enforceTwoFa = TwoFactorAuth::shouldEnforce($user); return response()->json([ diff --git a/src/Http/Controllers/Internal/v1/UserController.php b/src/Http/Controllers/Internal/v1/UserController.php index b08291c..19ff260 100644 --- a/src/Http/Controllers/Internal/v1/UserController.php +++ b/src/Http/Controllers/Internal/v1/UserController.php @@ -14,6 +14,7 @@ use Fleetbase\Models\Company; use Fleetbase\Models\CompanyUser; use Fleetbase\Models\Invite; +use Fleetbase\Models\Setting; use Fleetbase\Models\User; use Fleetbase\Notifications\UserAcceptedCompanyInvite; use Fleetbase\Notifications\UserInvited; @@ -410,7 +411,6 @@ public static function getWithDriver($id, Request $request) /** * Validate the user's current password. * - * @param \Fleetbase\Http\Requests\Internal\ValidatePasswordRequest $request * @return \Illuminate\Http\Response */ public function validatePassword(ValidatePasswordRequest $request) @@ -421,13 +421,12 @@ public function validatePassword(ValidatePasswordRequest $request) /** * Change the user's password. * - * @param \Fleetbase\Http\Requests\Internal\UpdatePasswordRequest $request * @return \Illuminate\Http\Response */ public function changeUserPassword(UpdatePasswordRequest $request) { - $user = $request->user(); - $newPassword = $request->input('password'); + $user = $request->user(); + $newPassword = $request->input('password'); $newConfirmPassword = $request->input('password_confirmation'); if ($newPassword !== $newConfirmPassword) { @@ -438,4 +437,37 @@ public function changeUserPassword(UpdatePasswordRequest $request) return response()->json(['status' => 'ok']); } + + /** + * Save the user selected locale. + * + * @return \Illuminate\Http\Response + */ + public function setUserLocale(Request $request) + { + $locale = $request->input('locale', 'en-us'); + $user = $request->user(); + $localeSettingKey = 'user.' . $user->uuid . '.locale'; + + // Persist to database + Setting::configure($localeSettingKey, $locale); + + return response()->json(['status' => 'ok']); + } + + /** + * Get the user selected locale. + * + * @return \Illuminate\Http\Response + */ + public function getUserLocale(Request $request) + { + $user = $request->user(); + $localeSettingKey = 'user.' . $user->uuid . '.locale'; + + // Get from database + $locale = Setting::lookup($localeSettingKey, 'en-us'); + + return response()->json(['status' => 'ok', 'locale' => $locale]); + } } diff --git a/src/Http/Requests/Internal/ValidatePasswordRequest.php b/src/Http/Requests/Internal/ValidatePasswordRequest.php index 4f6a5b2..4ce575b 100644 --- a/src/Http/Requests/Internal/ValidatePasswordRequest.php +++ b/src/Http/Requests/Internal/ValidatePasswordRequest.php @@ -5,7 +5,8 @@ use Fleetbase\Http\Requests\FleetbaseRequest; use Illuminate\Contracts\Validation\Rule; -class ConfirmCurrentPassword implements Rule { +class ConfirmCurrentPassword implements Rule +{ protected $user; public function __construct($user) @@ -56,7 +57,7 @@ public function authorize() public function rules() { return [ - 'password' => ['required', 'string', 'min:4', 'confirmed', new ConfirmCurrentPassword($this->user())], + 'password' => ['required', 'string', 'min:4', 'confirmed', new ConfirmCurrentPassword($this->user())], 'password_confirmation' => ['required', 'string'], ]; } diff --git a/src/Http/Requests/LoginRequest.php b/src/Http/Requests/LoginRequest.php index 7557bba..08446a6 100644 --- a/src/Http/Requests/LoginRequest.php +++ b/src/Http/Requests/LoginRequest.php @@ -56,7 +56,7 @@ public function messages() 'identity.required' => 'A email is required', 'identity.exists' => 'No user found by this email', 'identity.email' => 'Email used is invalid', - 'password.required' => 'A password is required', + 'password.required' => 'A password is required', ]; } } diff --git a/src/Listeners/HandleAccountCreated.php b/src/Listeners/HandleAccountCreated.php new file mode 100644 index 0000000..34a30d7 --- /dev/null +++ b/src/Listeners/HandleAccountCreated.php @@ -0,0 +1,38 @@ +user; + + if ($user) { + // Create and send verification code + try { + VerificationCode::generateEmailVerificationFor($user); + } catch (\Throwable $e) { + // If phone number is supplied send via SMS + if ($user->phone) { + try { + VerificationCode::generateSmsVerificationFor($user); + } catch (\Throwable $e) { + // silence + } + } + } + } + } +} diff --git a/src/Models/User.php b/src/Models/User.php index 74f0733..ebc373f 100644 --- a/src/Models/User.php +++ b/src/Models/User.php @@ -4,7 +4,6 @@ use Fleetbase\Casts\Json; use Fleetbase\Notifications\UserInvited; -use Fleetbase\Support\Auth; use Fleetbase\Support\Utils; use Fleetbase\Traits\Expandable; use Fleetbase\Traits\Filterable; @@ -425,10 +424,7 @@ public function changePassword($newPassword): User } /** - * Checks if password provided is the correct and current password for the user - * - * @param string $password - * @return boolean + * Checks if password provided is the correct and current password for the user. */ public function checkPassword(string $password): bool { @@ -608,7 +604,7 @@ public function getIdentity(): ?string return $phone; } - + /** * Check if the user is verified. * diff --git a/src/Providers/EventServiceProvider.php b/src/Providers/EventServiceProvider.php index 8cf3c4f..ca59556 100644 --- a/src/Providers/EventServiceProvider.php +++ b/src/Providers/EventServiceProvider.php @@ -16,6 +16,7 @@ class EventServiceProvider extends ServiceProvider * Fleetbase Events */ \Fleetbase\Events\ResourceLifecycleEvent::class => [\Fleetbase\Listeners\SendResourceLifecycleWebhook::class], + \Fleetbase\Events\AccountCreated::class => [\Fleetbase\Listeners\HandleAccountCreated::class], /* * Framework Events diff --git a/src/Support/TwoFactorAuth.php b/src/Support/TwoFactorAuth.php index c7ece2b..c6d11b8 100644 --- a/src/Support/TwoFactorAuth.php +++ b/src/Support/TwoFactorAuth.php @@ -9,9 +9,9 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Arr; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Redis; use Illuminate\Support\HtmlString; use Illuminate\Support\Str; -use Illuminate\Support\Facades\Redis; /** * Class TwoFactorAuth. @@ -321,9 +321,9 @@ public static function isEnabled(User $user): bool public static function shouldEnforce(User $user): bool { - $systemEnforced = static::isSystemEnforced(); + $systemEnforced = static::isSystemEnforced(); $companyEnforced = static::isCompanyEnforced($user->company); - $userEnabled = static::isEnabled($user); + $userEnabled = static::isEnabled($user); return $userEnabled ? !$userEnabled : $systemEnforced || $companyEnforced; } @@ -340,6 +340,7 @@ public static function isCompanyEnforced(Company $company): bool if ($twoFaSettings) { return $twoFaSettings->getBoolean('enforced'); } + return false; } diff --git a/src/Types/Country.php b/src/Types/Country.php index 6b6ef63..aa7578d 100644 --- a/src/Types/Country.php +++ b/src/Types/Country.php @@ -39,6 +39,13 @@ class Country implements \JsonSerializable */ protected $emoji; + /** + * The country languages. + * + * @var array + */ + protected $languages; + /** * Country Data. * @@ -63,11 +70,12 @@ public function __construct($code) $data = static::all()->where('cca2', $code)->first(); } - $this->name = $data['name'] = Utils::or($data, ['name.common', 'name.official', 'name_long', 'name_en']); - $this->currency = $data['currency'] = Utils::or($data, ['currencies.0', 'currencies.0.name']); - $this->emoji = $data['emoji'] = Utils::get($data, 'flag.emoji'); - $this->code = $code; - $this->data = $data; + $this->name = $data['name'] = Utils::or($data, ['name.common', 'name.official', 'name_long', 'name_en']); + $this->currency = $data['currency'] = Utils::or($data, ['currencies.0', 'currencies.0.name']); + $this->emoji = $data['emoji'] = Utils::get($data, 'flag.emoji'); + $this->languages = $data['languages'] = Utils::get($data, 'languages'); + $this->code = $code; + $this->data = $data; foreach ($data as $key => $value) { $this->{$key} = $value; diff --git a/src/routes.php b/src/routes.php index f138df3..cd6de13 100644 --- a/src/routes.php +++ b/src/routes.php @@ -189,6 +189,8 @@ function ($router, $controller) { $router->post('change-password', $controller('changeUserPassword')); $router->post('two-fa', $controller('saveTwoFactorSettings')); $router->get('two-fa', $controller('getTwoFactorSettings')); + $router->post('locale', $controller('setUserLocale')); + $router->get('locale', $controller('getUserLocale')); } ); $router->fleetbaseRoutes('user-devices');