From 97f0d1e3f7c2d84493b254c1c499b3245c22d06f Mon Sep 17 00:00:00 2001 From: 8tuso Date: Mon, 4 May 2026 20:59:36 +0300 Subject: [PATCH 1/3] Plugin information update --- auth-2fa/plugin.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/auth-2fa/plugin.php b/auth-2fa/plugin.php index fa82b3ae..aef33c43 100644 --- a/auth-2fa/plugin.php +++ b/auth-2fa/plugin.php @@ -2,9 +2,9 @@ return array( 'id' => '2fa:auth', # notrans - 'version' => '0.3', + 'version' => '0.4', 'name' => /* trans */ 'Two Factor Authenticator', - 'author' => 'Adriane Alexander', + 'author' => 'Adriane Alexander & Talal Alhayek', 'description' => /* trans */ 'Provides 2 Factor Authentication using an Authenticator App', 'url' => 'https://www.osticket.com/download', From 9ec39282300bdb24faf202a6d28885647c25581e Mon Sep 17 00:00:00 2001 From: 8tuso Date: Mon, 4 May 2026 21:00:16 +0300 Subject: [PATCH 2/3] New bootstrap added --- auth-2fa/auth2fa.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/auth-2fa/auth2fa.php b/auth-2fa/auth2fa.php index 3d98d4cb..3c8d3cda 100644 --- a/auth-2fa/auth2fa.php +++ b/auth-2fa/auth2fa.php @@ -12,7 +12,8 @@ function bootstrap() { if ($config->get('custom_issuer')) Auth2FABackend::$custom_issuer = $config->get('custom_issuer'); - TwoFactorAuthenticationBackend::register('Auth2FABackend'); + Staff2FABackend::register('Auth2FABackend'); + User2FABackend::register('UserAuth2FABackend'); } function enable() { From 334cc351153f1f96581b5da041bfaff6e368baac Mon Sep 17 00:00:00 2001 From: 8tuso Date: Mon, 4 May 2026 21:01:05 +0300 Subject: [PATCH 3/3] Heavy modifications and updates to the auth2fa file this will have all the new main logic --- auth-2fa/class.auth2fa.php | 149 ++++++++++++++++++++++++++++++------- 1 file changed, 123 insertions(+), 26 deletions(-) diff --git a/auth-2fa/class.auth2fa.php b/auth-2fa/class.auth2fa.php index 713956eb..c846cb4f 100644 --- a/auth-2fa/class.auth2fa.php +++ b/auth-2fa/class.auth2fa.php @@ -12,10 +12,30 @@ class Auth2FABackend extends TwoFactorAuthenticationBackend { protected function getSetupOptions() { global $thisstaff; + global $thisclient; + + $currentUser = null; + $auth2FA = null; + $email = null; + switch (true) { + case !empty($thisstaff) && ($thisstaff instanceof Staff || $thisstaff instanceof StaffSession): + $currentUser = $thisstaff; + $auth2FA = new Auth2FABackend; + $email = $currentUser->getEmail(); + break; + case !empty($thisclient) && ($thisclient instanceof ClientAccount || $thisclient instanceof UserAccount || $thisclient instanceof ClientSession): + $user = $thisclient->getUser(true); + $currentUser = $user->getAccount(); + $auth2FA = new UserAuth2FABackend; + $email = $user->getEmail(); + break; + default: + error_log("class of: " . get_class($thisclient)); + return false; + } - $auth2FA = new Auth2FABackend; - $qrCodeURL = $auth2FA->getQRCode($thisstaff); - if ($auth2FA->validateQRCode($thisstaff)) { + $qrCodeURL = $auth2FA->getQRCode($currentUser); + if ($auth2FA->validateQRCode($currentUser)) { return array( '' => new FreeTextField(array( 'configuration' => array( @@ -31,7 +51,7 @@ protected function getSetupOptions() { QR Code ', - $thisstaff->getEmail(), $qrCodeURL), + $email, $qrCodeURL), ) )), ); @@ -60,17 +80,17 @@ function validate($form, $user) { if (!($form->isValid() && ($clean=$form->getClean()) && $clean['token'])) - return false; + return false; // keep this if (!$this->validateLoginCode($clean['token'])) - return false; + return false; // keep this // upstream validation might throw an exception due to expired token // or too many attempts (timeout). It's the responsibility of the // caller to catch and handle such exceptions. $secretKey = $this->getSecretKey(); if (!$this->_validate($secretKey)) - return false; + return false; // keep this // Validator doesn't do house cleaning - it's our responsibility $this->onValidate($user); @@ -82,6 +102,10 @@ function send($user) { global $cfg; // Get backend configuration for this user + + global $thisclient; + if ($thisclient) $user = $thisclient->getUser(true)->getAccount(); + if (!$cfg || !($info = $user->get2FAConfig($this->getId()))) return false; @@ -98,16 +122,46 @@ function send($user) { } function store($secretKey) { - global $thisstaff; + $nameLookUp = null; + $currentUser = null; + + global $thisstaff, $thisclient; + if ($thisstaff) { + $currentUser = $thisstaff; + $nameLookUp = 'staff.'; + } + elseif ($thisclient) { + $currentUser = $thisclient->getUser(true)->getAccount(); + $nameLookUp = 'user.'; + } + + + if (empty($secretKey)) { + return false; + } $store = &$_SESSION['_2fa'][$this->getId()]; $store = ['otp' => $secretKey, 'time' => time(), 'strikes' => 0]; - if ($thisstaff) { + if ($currentUser) { + $userValue = null; + switch (true) { + case $currentUser instanceof ClientAccount: + $userValue = $currentUser->getUserId(); + break; + case $currentUser instanceof User: + $userValue = $currentUser->getId(); + break; + case $currentUser instanceof Staff || $currentUser instanceof StaffSession: + $userValue = $currentUser->getId(); + break; + default: + return false; + } $config = array('config' => array('key' => $secretKey, 'external2fa' => true)); - $_config = new Config('staff.'.$thisstaff->getId()); + $_config = new Config($nameLookUp.$userValue); $_config->set($this->getId(), JsonDataEncoder::encode($config)); - $thisstaff->_config = $_config->getInfo(); + $currentUser->_config = $_config->getInfo(); $errors['err'] = ''; } @@ -121,13 +175,34 @@ function validateLoginCode($code) { return $auth2FA->checkCode($secretKey, $code); } - function getSecretKey($staff=false) { - if (!$staff) { - $s = StaffAuthenticationBackend::getUser(); - $staff = Staff::lookup($s->getId()); + function getSecretKey($currentUser=false) { + global $thisstaff, $thisclient; + $userValue = null; + $nameLookUp = null; + if ($currentUser instanceof Staff || $currentUser instanceof StaffSession) { + $userValue = $currentUser->getId(); + $nameLookUp = 'staff.'; + } + elseif ($currentUser instanceof ClientAccount) { + $user = $currentUser->getUser(true); + $userValue = $user->getId(); + $nameLookUp = 'user.'; + } + elseif ($currentUser instanceof User || $currentUser instanceof ClientSession) { + $userValue = $currentUser->getId(); + $nameLookUp = 'user.'; + } + elseif ($thisstaff) { + $userValue = $thisstaff->getId(); + $nameLookUp = 'staff.'; + } + elseif ($thisclient) { + $user = $thisclient->getUser(true); + $userValue = $user->getId(); + $nameLookUp = 'user.'; } - if (!$token = ConfigItem::getConfigsByNamespace('staff.'.$staff->getId(), static::$id)) { + if (!$token = ConfigItem::getConfigsByNamespace($nameLookUp.$userValue, static::$id)) { $auth2FA = new \Sonata\GoogleAuthenticator\GoogleAuthenticator(); $this->secretKey = $auth2FA->generateSecret(); $this->store($this->secretKey); @@ -142,27 +217,49 @@ function getSecretKey($staff=false) { return $key; } - function getQRCode($staff=false) { - $staffEmail = $staff->getEmail(); - $secretKey = $this->getSecretKey($staff); + function getQRCode($currentUser=false) { + if ($currentUser instanceof ClientAccount) $userEmail = $currentUser->getUser()->getEmail(); + else $userEmail = $currentUser->getEmail(); + $secretKey = $this->getSecretKey($currentUser); $title = preg_replace('/[^A-Za-z0-9]/', '', self::$custom_issuer ?: __('osTicket')); - - return \Sonata\GoogleAuthenticator\GoogleQrUrl::generate($staffEmail, $secretKey, $title); + return \Sonata\GoogleAuthenticator\GoogleQrUrl::generate($userEmail, $secretKey, $title); } - function validateQRCode($staff=false) { + function validateQRCode($currentUser=false) { $auth2FA = new \Sonata\GoogleAuthenticator\GoogleAuthenticator(); - $secretKey = $this->getSecretKey($staff); - $code = self::getCode(); + $secretKey = $this->getSecretKey($currentUser); + // Fix: Generate code using the CURRENT secret key, not a new backend instance + $code = $auth2FA->getCode($secretKey); return $auth2FA->checkCode($secretKey, $code); } - static function getCode() { + static function getCode($currentUser=false) { + if (!$currentUser) { + global $thisstaff, $thisclient; + if ($thisstaff) { + $currentUser = $thisstaff; + $self = new Auth2FABackend(); + } + elseif ($thisclient) { + $currentUser = $thisclient->getUser(true)->getAccount(); + $self = new UserAuth2FABackend(); + } + } + $auth2FA = new \Sonata\GoogleAuthenticator\GoogleAuthenticator(); - $self = new Auth2FABackend(); $secretKey = $self->getSecretKey(); return $auth2FA->getCode($secretKey); } } + +class UserAuth2FABackend extends Auth2FABackend { + static $id = "auth.user"; + static $name = "Authenticator"; + + static $desc = /* @trans */ 'Verification codes are located in the Authenticator app of your choice on your phone'; + static $custom_issuer; + + var $secretKey; +}