[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