diff --git a/src/Http/Controllers/Internal/v1/AuthController.php b/src/Http/Controllers/Internal/v1/AuthController.php
index 50aca1f..e3682be 100644
--- a/src/Http/Controllers/Internal/v1/AuthController.php
+++ b/src/Http/Controllers/Internal/v1/AuthController.php
@@ -17,11 +17,13 @@
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;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Str;
+use Laravel\Sanctum\PersonalAccessToken;
class AuthController extends Controller
{
@@ -32,14 +34,43 @@ class AuthController extends Controller
*/
public function login(LoginRequest $request)
{
- $email = $request->input('email');
- $password = $request->input('password');
- $user = User::where('email', $email)->first();
+ $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]);
+ }
+ }
+
+ // Find the user using the identity provided
+ $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, ['code' => 'no_user']);
+ return response()->error('No user found by the provided identity.', 401, ['code' => 'no_user']);
}
+ // Check if 2FA enabled
+ if (TwoFactorAuth::isEnabled($user)) {
+ $twoFaSession = TwoFactorAuth::start($user);
+
+ return response()->json([
+ 'twoFaSession' => $twoFaSession,
+ 'isEnabled' => true,
+ ]);
+ }
+
+ // 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']);
}
@@ -418,8 +449,7 @@ public function signUp(SignUpRequest $request)
$companyDetails = $request->input('company');
$newUser = Auth::register($userDetails, $companyDetails);
-
- $token = $newUser->createToken($request->ip());
+ $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 4d53d31..a91b3f7 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,46 @@ 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 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);
+ }
+ if (isset($twoFaSettings['enabled']) && $twoFaSettings['enabled'] === false) {
+ $twoFaSettings['enforced'] = false;
+ }
+ TwoFactorAuth::saveTwoFaSettingsForCompany($company, $twoFaSettings);
+
+ return response()->json(['message' => 'Two-Factor Authentication saved successfully']);
+ }
}
diff --git a/src/Http/Controllers/Internal/v1/TwoFaController.php b/src/Http/Controllers/Internal/v1/TwoFaController.php
new file mode 100644
index 0000000..41f262c
--- /dev/null
+++ b/src/Http/Controllers/Internal/v1/TwoFaController.php
@@ -0,0 +1,166 @@
+array('twoFaSettings');
+ if (isset($twoFaSettings['enabled']) && $twoFaSettings['enabled'] === false) {
+ $twoFaSettings['enforced'] = false;
+ }
+ $settings = TwoFactorAuth::configureTwoFaSettings($twoFaSettings);
+
+ return response()->json($settings->value);
+ }
+
+ /**
+ * Get Two-Factor Authentication system wide settings.
+ *
+ * @return \Illuminate\Http\Response
+ */
+ public function getSystemConfig()
+ {
+ $settings = TwoFactorAuth::getTwoFaConfiguration();
+
+ return response()->json($settings->value);
+ }
+
+ /**
+ * Check Two-Factor Authentication status for a given user identity.
+ *
+ * @return \Illuminate\Http\Response
+ */
+ public function checkTwoFactor(Request $request)
+ {
+ $identity = $request->input('identity');
+ $twoFaSession = TwoFactorAuth::createTwoFaSessionIfEnabled($identity);
+ $isTwoFaEnabled = $twoFaSession !== null;
+
+ return response()->json([
+ 'twoFaSession' => $twoFaSession,
+ 'isTwoFaEnabled' => $isTwoFaEnabled,
+ ]);
+ }
+
+ /**
+ * Verify Two-Factor Authentication code.
+ *
+ * @return \Illuminate\Http\Response
+ */
+ public function validateSession(TwoFaValidationRequest $request)
+ {
+ $token = $request->input('token');
+ $identity = $request->input('identity');
+ $clientToken = $request->input('clientToken');
+
+ try {
+ $validClientToken = TwoFactorAuth::getClientSessionTokenFromTwoFaSession($token, $identity, $clientToken);
+
+ return response()->json([
+ 'clientToken' => $validClientToken,
+ 'expired' => false,
+ ]);
+ } catch (\Exception $e) {
+ $errorMessage = $e->getMessage();
+
+ if (Str::contains($errorMessage, ['2FA Verification', 'expired'])) {
+ return response()->json([
+ 'expired' => true,
+ ]);
+ }
+
+ return response()->error($errorMessage);
+ }
+ }
+
+ /**
+ * Verify Two-Factor Authentication code.
+ *
+ * @return \Illuminate\Http\Response
+ */
+ public function verifyCode(Request $request)
+ {
+ $code = $request->input('code');
+ $token = $request->input('token');
+ $clientToken = $request->input('clientToken');
+
+ try {
+ $authToken = TwoFactorAuth::verifyCode($code, $token, $clientToken);
+
+ return response()->json([
+ 'authToken' => $authToken,
+ ]);
+ } catch (\Exception $e) {
+ return response()->error($e->getMessage());
+ }
+ }
+
+ /**
+ * Resend Two-Factor Authentication verification code.
+ *
+ * @return \Illuminate\Http\Response
+ */
+ public function resendCode(Request $request)
+ {
+ $identity = $request->input('identity');
+ $token = $request->input('token');
+
+ try {
+ $clientToken = TwoFactorAuth::resendCode($identity, $token);
+
+ return response()->json([
+ 'clientToken' => $clientToken,
+ ]);
+ } catch (\Exception $e) {
+ return response()->error($e->getMessage());
+ }
+ }
+
+ /**
+ * Invalidate the current two-factor session.
+ *
+ * @return \Illuminate\Http\Response
+ */
+ public function invalidateSession(Request $request)
+ {
+ $identity = $request->input('identity');
+ $token = $request->input('token');
+
+ try {
+ $ok = TwoFactorAuth::forgetTwoFaSession($token, $identity);
+
+ return response()->json([
+ 'ok' => $ok,
+ ]);
+ } catch (\Exception $e) {
+ 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/Http/Controllers/Internal/v1/UserController.php b/src/Http/Controllers/Internal/v1/UserController.php
index 108b951..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;
@@ -17,10 +18,12 @@
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;
use Illuminate\Support\Carbon;
+use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Maatwebsite\Excel\Facades\Excel;
@@ -53,6 +56,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.
*
@@ -367,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/Http/Requests/LoginRequest.php b/src/Http/Requests/LoginRequest.php
index 78212f6..7557bba 100644
--- a/src/Http/Requests/LoginRequest.php
+++ b/src/Http/Requests/LoginRequest.php
@@ -40,7 +40,7 @@ protected function failedValidation(Validator $validator)
public function rules()
{
return [
- 'email' => 'required|email|exists:users,email',
+ 'identity' => 'required|email|exists:users,email',
'password' => 'required',
];
}
@@ -53,9 +53,9 @@ public function rules()
public function messages()
{
return [
- 'email.required' => 'A email is required',
- 'email.exists' => 'No user found by this email',
- 'email.email' => 'Email used is invalid',
+ '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',
];
}
diff --git a/src/Http/Requests/TwoFaValidationRequest.php b/src/Http/Requests/TwoFaValidationRequest.php
new file mode 100644
index 0000000..944b087
--- /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/Mail/VerifyEmail.php b/src/Mail/VerifyEmail.php
index 13193bc..5a7a030 100644
--- a/src/Mail/VerifyEmail.php
+++ b/src/Mail/VerifyEmail.php
@@ -4,10 +4,11 @@
use Fleetbase\Models\VerificationCode;
use Illuminate\Bus\Queueable;
-// use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Database\Eloquent\Model;
use Illuminate\Mail\Mailable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Queue\SerializesModels;
+use Illuminate\Support\HtmlString;
class VerifyEmail extends Mailable
{
@@ -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 . '
Your two-factor authentication code is: ' . $verificationCode->code . '
'), + ]; + }; + + // create verification code + return VerificationCode::generateEmailVerificationFor($user, '2fa', $messageCallback, $linesCallback, [], $expiresAfter); + } + + throw new \Exception('Invalid 2FA method selected in settings.'); + } + + /** + * Create a Two-Factor Authentication session if enabled. + * + * @param string $identity the user identity + * + * @return string|null the Two-Factor Authentication session key, or null if not enabled + */ + public static function createTwoFaSessionIfEnabled(string $identity): ?string + { + $user = static::getUserFromIdentity($identity); + if (!$user) { + return null; + } + + $isTwoFaEnabled = self::isEnabled($user); + + if ($isTwoFaEnabled) { + return self::start($identity); + } + + return null; + } + + /** + * Check if Two-Factor Authentication is enabled. + * + * @return bool true if Two-Factor Authentication is enabled, false otherwise + */ + public static function isEnabled(User $user): bool + { + $twoFaSettings = static::getTwoFaSettingsForUser($user); + if (!$twoFaSettings) { + return false; + } + + 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. + * + * @param string $identity the user identity + * @param int $tokenLength the length of the generated token + * + * @return string|null the Two-Factor Authentication session key, or null on failure + */ + public static function start(string $identity, int $tokenLength = 40): ?string + { + $user = static::getUserFromIdentity($identity); + + if ($user) { + $token = Str::random($tokenLength); + $twoFaSessionKey = static::createTwoFaSessionKey($user, $token, true); + + return static::encryptSessionKey($twoFaSessionKey, $user->uuid); + } + + return null; + } + + /** + * Verify a Two-Factor Authentication code and return a user token. + * + * @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 verifyCode(string $code, string $token, string $clientToken): string + { + // 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.'); + } + + // 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 + Redis::del($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.'); + } + + /** + * 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 + * + * @throws \Exception if no user found or the Two-Factor Authentication session is invalid + */ + public static function resendCode(string $identity, string $token): string + { + $user = static::getUserFromIdentity($identity); + if (!$user) { + throw new \Exception('No user found using the provided identity'); + } + + // Make sure two factor session is valid + if (!static::validateSessionToken($token, $identity)) { + throw new \Exception('2FA session is invalid.'); + } + + // 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; + } + + /** + * 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 = Redis::exists($twoFaSessionKey); + + if ($exists) { + $parts = explode(':', $twoFaSessionKey); + $userId = Arr::get($parts, 1); + + return Str::isUuid($userId) && $userId === $user->uuid; + } + + return false; + } + + /** + * 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.'); + } + + // Get session key and destroy it + $twoFaSessionKey = static::decryptSessionKey($token, $user); + + return Redis::del($twoFaSessionKey); + } + + /** + * 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; + + if ($storeInCache) { + $expirationTime = Carbon::now()->addSeconds($expiresAfter)->timestamp; + Redis::set($twoFaSessionKey, $user->uuid, 'EX', $expirationTime); + } + + 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; + } + + /** + * 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 + { + $clientTokenDecoded = base64_decode($clientToken); + $clientTokenParts = explode('|', $clientTokenDecoded); + + 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]; + + if ($verificationCodeId) { + $verificationCode = VerificationCode::where('uuid', $verificationCodeId)->first(); + + if ($verificationCode) { + return $verificationCode; + } + } + + return null; + } + + /** + * 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/Support/Utils.php b/src/Support/Utils.php index 98d0ab7..b7b8e44 100644 --- a/src/Support/Utils.php +++ b/src/Support/Utils.php @@ -2199,4 +2199,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; + } } diff --git a/src/routes.php b/src/routes.php index 04e15a5..f138df3 100644 --- a/src/routes.php +++ b/src/routes.php @@ -87,12 +87,28 @@ 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) { $router->get('branding', 'SettingController@getBrandingSettings'); } ); + $router->group( + ['prefix' => 'two-fa'], + function ($router) { + $router->get('check', 'TwoFaController@checkTwoFactor'); + $router->post('validate', 'TwoFaController@validateSession'); + $router->post('verify', 'TwoFaController@verifyCode'); + $router->post('resend', 'TwoFaController@resendCode'); + $router->post('invalidate', 'TwoFaController@invalidateSession'); + } + ); $router->group( ['middleware' => ['fleetbase.protected']], function ($router) { @@ -136,6 +152,14 @@ function ($router, $controller) { $router->post('test-notification-channels-config', $controller('testNotificationChannelsConfig')); } ); + $router->fleetbaseRoutes( + 'two-fa', + function ($router, $controller) { + $router->post('config', $controller('saveSystemConfig')); + $router->get('config', $controller('getSystemConfig')); + $router->get('enforce', $controller('shouldEnforce')); + } + ); $router->fleetbaseRoutes('api-events'); $router->fleetbaseRoutes('api-request-logs'); $router->fleetbaseRoutes( @@ -146,7 +170,10 @@ function ($router, $controller) { } ); $router->fleetbaseRoutes('webhook-request-logs'); - $router->fleetbaseRoutes('companies'); + $router->fleetbaseRoutes('companies', function ($router, $controller) { + $router->get('two-fa', $controller('getTwoFactorSettings')); + $router->post('two-fa', $controller('saveTwoFactorSettings')); + }); $router->fleetbaseRoutes( 'users', function ($router, $controller) { @@ -158,6 +185,10 @@ 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')); } ); $router->fleetbaseRoutes('user-devices');