[TikiWiki-commits] [Git][tikiwiki/tiki][27.x] [FIX] Add TwoFactorAuth library to fix class not found in tiki-login.php

"Elifeleti Mukisa Dan \(@Danelif\) via TikiWiki-cvs" <[email protected]>
Newsgroups gmane.comp.cms.tiki.cvs
Message-ID <6a213da276d09_381905208191c7@gitlab-sidekiq-low-urgency-cpu-bound-v2-6564bf9646-fnd85.mail>

Elifeleti Mukisa Dan pushed to branch 27.x at Tiki Wiki CMS Groupware / Tiki


Commits:
c3845871 by Elifeleti Mukisa Dan at 2026-06-04T08:49:22+00:00
[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

- - - - -


6 changed files:

- + 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:

=====================================
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/c3845871da1e388815ab94aabf85ff10de1d4a83

-- 
View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/c3845871da1e388815ab94aabf85ff10de1d4a83
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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.