[TikiWiki-commits] [Git][tikiwiki/tiki][24.x] [BP][FIX] Add TwoFactorAuth library to fix class not found in tiki-login.php
"Victor Emanouilov \(@kroky\) via TikiWiki-cvs" <[email protected]> Mon, 06 Jul 2026 14:08:09 +0000
| Newsgroups | gmane.comp.cms.tiki.cvs |
|---|---|
| Message-ID | <6a4bb6c97eb22_3819aa50115f5@gitlab-sidekiq-low-urgency-cpu-bound-v2-757cb76464-l7pkf.mail> |
Victor Emanouilov pushed to branch 24.x at Tiki Wiki CMS Groupware / Tiki Commits: 87f4f81d by Elifeleti Mukisa Dan at 2026-07-06T14:00:36+00:00 [BP][FIX] Add TwoFactorAuth library to fix class not found in tiki-login.php --- * [FIX] Add TwoFactorAuth library to fix class not found in tiki-login.php --- * [FIX] Add twofactor classes to fix class not found See merge request tikiwiki/tiki!10438 (cherry picked from commit c3845871da1e388815ab94aabf85ff10de1d4a83) e7e66ef7 [FIX] Add twofactor classes to fix class not found Co-authored-by: Elifeleti Mukisa Dan <[email protected]> See merge request tikiwiki/tiki!10653 - - - - - 7 changed files: - .gitlab-ci.yml - + lib/core/TwoFactorAuth/Email2FA.php - + lib/core/TwoFactorAuth/Exception/Exception.php - + lib/core/TwoFactorAuth/Exception/TwoFactorAuthException.php - + lib/core/TwoFactorAuth/Google2FA.php - + lib/core/TwoFactorAuth/TwoFactorAuth.php - + lib/core/TwoFactorAuth/TwoFactorAuthInterface.php Changes: ===================================== .gitlab-ci.yml ===================================== @@ -14,6 +14,9 @@ variables: MYSQL_PASSWORD: tikipass ELASTICSEARCH_HOST: elasticsearch BASE_QA_IMAGE: tikiwiki/tikiwiki-ci:7.4-qa + # Composer 2.10+ refuses to parse tar/phar archives on PHP < 8.0 (the CI image is PHP 7.4). + # The archives come from trusted sources (packagist / composer.tiki.org), so allow it. + COMPOSER_ALLOW_UNSAFE_PHAR_METADATA: "1" workflow: rules: ===================================== lib/core/TwoFactorAuth/Email2FA.php ===================================== @@ -0,0 +1,163 @@ +<?php + +// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project +// +// All Rights Reserved. See copyright.txt for details and a complete list of authors. +// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details. +namespace Tiki\TwoFactorAuth; + +use Exception; +use Symfony\Component\HttpFoundation\Session\Session; +use Tiki\TwoFactorAuth\Exception\TwoFactorAuthException; +use TikiDb; +use TikiLib; +use TikiMail; + +class Email2FA implements TwoFactorAuthInterface +{ + private $twoFATable; + private $userlib; + private $session; + private $crypt; + + public function __construct() + { + $this->twoFATable = TikiDb::get()->table('tiki_2fa_email_tokens'); + $this->userlib = TikiLib::lib('user'); + $this->session = new Session(); + $this->crypt = TikiLib::lib('crypt'); + } + + public function generateCode($user, $isEmail = true) + { + global $prefs; + + $lastRequestTime = $this->session->get('last_2fa_request_time') ?? 0; + if (time() - $lastRequestTime < 60) { + $errMsg = tr('Please wait about 60 seconds before requesting a new 2fa token.'); + throw new TwoFactorAuthException($errMsg); + } + + $userInfo = $this->userlib->get_user_info($user); + if (empty($userInfo)) { + $errMsg = tr('User does not exist.'); + throw new TwoFactorAuthException($errMsg); + } + + $tokenLength = intval($prefs['twoFactorAuthEmailTokenLength'] ?? 6); + $token = $this->generateRandomString($tokenLength, $prefs['twoFactorAuthEmailTokenChars'] ?? ''); + + if ($isEmail) { + try { + $mail = new TikiMail(); + $mail->setSubject(tr('Your 2FA Token')); + $mail->setText(tr('Your token is: ') . $token); + if (empty($userInfo['email'])) { + $errMsg = tr('User email is not set.'); + throw new TwoFactorAuthException($errMsg); + } + $mail->send([$userInfo['email']]); + } catch (Exception $e) { + $errMsg = tr('Failed to send email: ' . $e->getMessage()); + throw new TwoFactorAuthException($errMsg); + } + } + + $userId = $userInfo['userId']; + $hashedToken = $this->crypt->encryptData($token); + + $insertDetails = [ + 'userId' => $userId, + 'token' => $hashedToken, + 'type' => 'email', + 'attempts' => 0, + 'created' => time() + ]; + + $isInserted = $this->twoFATable->insertOrUpdate($insertDetails, ['userId' => $userId]); + + if (! $isInserted) { + $errMsg = tr('Failed to insert 2fa token.'); + throw new TwoFactorAuthException($errMsg); + } + + $this->session->set('last_2fa_request_time', time()); + + return ! $isEmail ? $token : true; + } + + public function validateCode($user, $code = null) + { + if (empty($code)) { + $errMsg = tr('You have enabled 2FA and 2FA code is required. So, you should login with 2FA.'); + throw new TwoFactorAuthException($errMsg); + } + + global $prefs; + + $userInfo = $this->userlib->get_user_info($user); + if (empty($userInfo)) { + $errMsg = tr('User does not exist.'); + throw new TwoFactorAuthException($errMsg); + } + + $tokenInfo = $this->twoFATable->fetchFullRow(['userId' => $userInfo['userId']]); + if (empty($tokenInfo)) { + $errMsg = tr('2FA token info does not exist.'); + throw new TwoFactorAuthException($errMsg); + } + + if (intval($tokenInfo['attempts']) >= 3) { + $errMsg = tr('Attempt limit exceeded. Please request a new 2fa token.'); + throw new TwoFactorAuthException($errMsg); + } + + $hashedTokenFromDb = $tokenInfo['token']; + $hashedTokenFromClient = $this->crypt->encryptData($code); + $attempts = $tokenInfo['attempts']; + $created = intval($tokenInfo['created']); + $tokenTTL = intval($prefs['twoFactorAuthEmailTokenTTL'] ?? 30) * 60; + + if (time() - $created > $tokenTTL) { + $errMsg = tr('2FA token has expired. Please request a new one from login page.'); + throw new TwoFactorAuthException($errMsg); + } + + if ($hashedTokenFromDb !== $hashedTokenFromClient) { + $attempts = $tokenInfo['attempts'] + 1; + $this->twoFATable->update(['attempts' => $attempts], ['userId' => $userInfo['userId']]); + return false; + } + + return true; + } + + private function generateRandomString($length = 6, $chars = '') + { + if (! empty($chars)) { + $list = []; + if (preg_match_all('/(.)-(.)/', $chars, $m)) { + foreach ($m[0] as $k => $_) { + $class = ''; + for ($i = ord($m[1][$k]); $i <= max(ord($m[1][$k]), ord($m[2][$k])); $i++) { + $class .= chr($i); + } + $list[] = $class; + } + $chars = str_replace($m[0], '', $chars); + } + if ($chars) { + $list[] = $chars; + } + } else { + $list = ['aeiou', 'AEIOU', 'bcdfghjklmnpqrstvwxyz', 'BCDFGHJKLMNPQRSTVWXYZ', '0123456789']; + } + shuffle($list); + $randomString = ''; + for ($i = 0; $i < $length; $i++) { + $ch = $list[$i % count($list)]; + $randomString .= $ch[rand(0, strlen($ch) - 1)]; + } + return $randomString; + } +} ===================================== lib/core/TwoFactorAuth/Exception/Exception.php ===================================== @@ -0,0 +1,17 @@ +<?php + +// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project +// +// All Rights Reserved. See copyright.txt for details and a complete list of authors. +// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details. +namespace Tiki\TwoFactorAuth\Exception; + +use Exception as BaseException; + +class Exception extends BaseException +{ + public function __construct(string $message, ?BaseException $previous = null, int $code = 0) + { + parent::__construct($message, $code, $previous); + } +} ===================================== lib/core/TwoFactorAuth/Exception/TwoFactorAuthException.php ===================================== @@ -0,0 +1,15 @@ +<?php + +// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project +// +// All Rights Reserved. See copyright.txt for details and a complete list of authors. +// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details. +namespace Tiki\TwoFactorAuth\Exception; + +class TwoFactorAuthException extends Exception +{ + public function __construct(string $message, ?Exception $previous = null, int $code = 0) + { + parent::__construct($message, $previous, $code); + } +} ===================================== lib/core/TwoFactorAuth/Google2FA.php ===================================== @@ -0,0 +1,46 @@ +<?php + +// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project +// +// All Rights Reserved. See copyright.txt for details and a complete list of authors. +// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details. +namespace Tiki\TwoFactorAuth; + +use TikiLib; +use PragmaRX\Google2FA\Google2FA as PragmaGoogle2FA; +use Tiki\TwoFactorAuth\Exception\TwoFactorAuthException; + +class Google2FA implements TwoFactorAuthInterface +{ + private $google2fa; + private $userlib; + + public function __construct() + { + $this->google2fa = new PragmaGoogle2FA(); + $this->userlib = TikiLib::lib('user'); + } + + public function generateCode($user, $isEmail = true) + { + // For Google2FA, code generation is typically done on the client-side + return true; + } + + public function validateCode($user, $code = null) + { + if (empty($code)) { + $errMsg = tr('You have enabled 2FA and 2FA code is required. So, you should login with 2FA.'); + throw new TwoFactorAuthException($errMsg); + } + + $twoFactorSecret = $this->userlib->get_2_factor_secret($user); + $result = $this->google2fa->verifyKey($twoFactorSecret, $code, 2); + + if (! $result) { + $this->userlib->handleUnsuccessfulLogin($user); + } + + return $result; + } +} ===================================== lib/core/TwoFactorAuth/TwoFactorAuth.php ===================================== @@ -0,0 +1,96 @@ +<?php + +// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project +// +// All Rights Reserved. See copyright.txt for details and a complete list of authors. +// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details. +namespace Tiki\TwoFactorAuth; + +use Tiki\TwoFactorAuth\Exception\TwoFactorAuthException; +use TikiLib; + +class TwoFactorAuth +{ + /** @var string string representation for google 2FA */ + public const GOOGLE_2FA = 'google2FA'; + + /** @var string string representation for email 2FA */ + public const EMAIL_2FA = 'email2FA'; + + /** @var string The default 2FA type */ + public const DEFAULT_2FA = self::GOOGLE_2FA; + + /** @var string[] The list of available 2FA types */ + public const AVAILABLE_2FA_TYPES = [self::GOOGLE_2FA, self::EMAIL_2FA]; + + public static function getTwoFactorAuthTypeEnabled(): string + { + global $prefs; + + // If not set or empty, always default to the default type + return $prefs['twoFactorAuthType'] ?: self::DEFAULT_2FA; + } + + public static function getTwoFactorAuth() + { + return self::getTwoFactorAuthByType(self::getTwoFactorAuthTypeEnabled()); + } + + public static function getTwoFactorAuthByType($type) + { + $authType = ucfirst($type); + $class = "\\Tiki\\TwoFactorAuth\\$authType"; + + if (! in_array($type, self::AVAILABLE_2FA_TYPES, true) || ! class_exists($class)) { + $errMsg = tr('Two factor auth type not found: ' . $type . ', Supported types are: ' . implode(', ', self::AVAILABLE_2FA_TYPES)); + throw new TwoFactorAuthException($errMsg); + } + + if (! in_array(TwoFactorAuthInterface::class, class_implements($class), true)) { + $errMsg = tr('The class ' . $class . ' does not implement the required TwoFactorAuthInterface.'); + throw new TwoFactorAuthException($errMsg); + } + + $twoFactorAuth = new $class(); + + return $twoFactorAuth; + } + + public static function isMFARequired($user) + { + global $prefs, $userlib; + + $mfaIntervalDaysPrefs = intval($prefs['twoFactorAuthIntervalDays']); + $requireMfa = false; + + if ($prefs['twoFactorAuth'] == 'y') { + $userInfo = $userlib->get_user_info($user); + if (! empty($userInfo['twoFactorSecret'])) { + $lastMfaDateDb = intval($userInfo['last_mfa_date']); + if ($mfaIntervalDaysPrefs > 0) { + if (empty($lastMfaDateDb) || (time() - $lastMfaDateDb) > ($mfaIntervalDaysPrefs * 86400)) { + $requireMfa = true; + } + } else { + $requireMfa = true; + } + } + } + + return $requireMfa; + } + + public static function get2FactorSecret($user) + { + $userlib = TikiLib::lib('user'); + $twoFAType = self::getTwoFactorAuthTypeEnabled(); + + if ($twoFAType === self::GOOGLE_2FA) { + return $userlib->get_2_factor_secret($user); + } elseif ($twoFAType === self::EMAIL_2FA) { + return true; + } else { + throw new TwoFactorAuthException(tr('Unsupported 2FA type: ' . $twoFAType)); + } + } +} ===================================== lib/core/TwoFactorAuth/TwoFactorAuthInterface.php ===================================== @@ -0,0 +1,13 @@ +<?php + +// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project +// +// All Rights Reserved. See copyright.txt for details and a complete list of authors. +// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details. +namespace Tiki\TwoFactorAuth; + +interface TwoFactorAuthInterface +{ + public function generateCode($user, $isEmail = true); + public function validateCode($user, $code = null); +} View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/87f4f81d8b31818b50b24ec2fac69c2c8648c71f -- View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/87f4f81d8b31818b50b24ec2fac69c2c8648c71f You're receiving this email because of your account on gitlab.com. Manage all notifications: https://gitlab.com/-/profile/notifications | Help: https://gitlab.com/help _______________________________________________ TikiWiki-cvs mailing list [email protected] https://lists.sourceforge.net/lists/listinfo/tikiwiki-cvs