[TikiWiki-commits] [Git][tikiwiki/tiki][master] [ENH] User validation: enhance login flow with ticket validation and failure recording options

"Espoir Baraka \(@esbarakabigega\) via TikiWiki-cvs" <[email protected]>
Newsgroups gmane.comp.cms.tiki.cvs
Message-ID <6a84ad348adc7_3835bd79c623cb@gitlab-sidekiq-low-urgency-cpu-bound-v2-789dc4448d-9khnn.mail>

Espoir Baraka pushed to branch master at Tiki Wiki CMS Groupware / Tiki


Commits:
a574dd13 by Espoir Baraka at 2026-08-18T20:49:27+02:00
[ENH] User validation: enhance login flow with ticket validation and failure recording options
---
* [ENH] User validation: enhance login flow with ticket validation and failure recording options

- Added a new parameter to `validate_user` and `validate_user_tiki` methods to control failure recording.
- Updated `actionValidateUser` to include CSRF protection and rate-limiting for login attempts.
- Modified the login box template to send a security ticket with user credentials.

(cherry picked from commit a82fe4f5a01fb46a382a4f214660215bb318e7bb)

See merge request tikiwiki/tiki!10958

- - - - -


3 changed files:

- lib/core/Services/User/Controller.php
- lib/userslib.php
- templates/modules/mod-login_box.tpl


Changes:

=====================================
lib/core/Services/User/Controller.php
=====================================
@@ -4,6 +4,9 @@
 //
 // 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.
+
+use Tiki\BruteForce\BruteForce;
+
 class Services_User_Controller
 {
     /**
@@ -1440,13 +1443,79 @@ class Services_User_Controller
         return true;
     }
 
+    /**
+     * Pre-check credentials for interactive login flows (e.g. 2FA step).
+     *
+     * Keep endpoint pre-auth but hardened: POST + CSRF (single-use ticket),
+     * and the same brute-force policy as tiki-login.
+     *
+     * @param JitFilter $input
+     * @return array{valid: bool, ticket: string|false}
+     * @throws Services_Exception
+     */
     public function actionValidateUser($input)
     {
+        global $prefs;
+
+        $access = TikiLib::lib('access');
+        $tikilib = TikiLib::lib('tiki');
+
+        if (! $access->requestIsPost()) {
+            throw new Services_Exception(tra('Method not allowed'), 405);
+        }
+
+        if (empty($_POST['ticket'])) {
+            throw new Services_Exception(tra('Missing security ticket'), 401);
+        }
+
+        $access->checkCsrf(false, true, 'hostTicket', true, '', 'services');
+        $access->setTicket();
+
         $username = $input->username->text();
         $password = $input->password->text();
+        $bruteForceProperties = ['ip' => $tikilib->get_ip_address()];
+
+        if ($username === '' || $password === '') {
+            return [
+                'valid' => false,
+                'ticket' => $access->getTicket(),
+            ];
+        }
+
+        if (($prefs['bruteforce_protection'] ?? 'n') === 'y') {
+            $bruteForce = new BruteForce();
+            if (! $bruteForce->isOperationAllowed('login', $bruteForceProperties, false)) {
+                $waitTime = $bruteForce->getWaitTime('login', $bruteForceProperties);
+                if ($waitTime > 60) {
+                    $message = sprintf(
+                        tra('Too many login attempts. Please try again in %d minutes and %d seconds.'),
+                        floor($waitTime / 60),
+                        $waitTime % 60
+                    );
+                } else {
+                    $message = sprintf(tra('Too many login attempts. Please try again in %d seconds.'), $waitTime);
+                }
+                throw new Services_Exception($message, 429);
+            }
+        }
+
         $userlib = TikiLib::lib('user');
-        $ret = $userlib->validate_user($username, $password);
-        return $ret[0];
+        $ret = $userlib->validate_user($username, $password, false, null, false);
+        $valid = (bool) $ret[0];
+
+        if (($prefs['bruteforce_protection'] ?? 'n') === 'y') {
+            $bruteForce = $bruteForce ?? new BruteForce();
+            if ($valid) {
+                $bruteForce->success('login', $bruteForceProperties);
+            } else {
+                $bruteForce->attempt('login', $bruteForceProperties);
+            }
+        }
+
+        return [
+            'valid' => $valid,
+            'ticket' => $access->getTicket(),
+        ];
     }
 
     public function action_save_column_prefs($input)


=====================================
lib/userslib.php
=====================================
@@ -428,8 +428,9 @@ class UsersLib extends TikiLib
     * @param twoFactorCode: ???
     * @param validate_phase: If true, user followed the link from validation email after creation of account
     * @param for_login: If true, we are validating user for login purpose. If false, we are only checking that user is valid (for check of current password for example)
+    * @param record_failure: If false, unsuccessful attempts are not recorded (no lockout / waiting side effects)
     */
-    public function validate_user($user, $pass, $validate_phase = false, $twoFactorCode = null, $for_login = true)
+    public function validate_user($user, $pass, $validate_phase = false, $twoFactorCode = null, $for_login = true, $record_failure = true)
     {
         global $prefs;
 
@@ -485,7 +486,7 @@ class UsersLib extends TikiLib
         // first attempt a login via the standard Tiki system
         //
         if (! ($auth_shib || $auth_cas || $auth_saml) || $isAdminGroupMember) { //redflo: does this mean, that users in cas and shib are not replicated to tiki tables? Does this work well?
-            list($result, $user) = $this->validate_user_tiki($user, $pass, $validate_phase);
+            list($result, $user) = $this->validate_user_tiki($user, $pass, $validate_phase, $record_failure);
         } else {
             $result = null;
         }
@@ -544,6 +545,9 @@ class UsersLib extends TikiLib
         if ($noSpecialAuthEnabled || $adminCanSkip || $ldapUserHasTikiLoginAccess) {
             // if the user verified ok, log them in
             if ($userTiki) {//user validated in tiki, update lastlogin and be done
+                if (! $for_login) {
+                    return [true, $user, $result];
+                }
                 if ($auth_ldap) {
                     return [$this->_ldap_sync_and_update_lastlogin($user, $pass), $user, $result, 'tiki'];
                 }
@@ -1949,7 +1953,7 @@ class UsersLib extends TikiLib
      * @param user: username
      * @param pass: password
      */
-    public function validate_user_tiki($user, $pass, $validate_phase = false)
+    public function validate_user_tiki($user, $pass, $validate_phase = false, $record_failure = true)
     {
         global $prefs;
 
@@ -2016,7 +2020,9 @@ class UsersLib extends TikiLib
                 }
                 return [USER_VALID, $user];
             } else {
-                $this->handleUnsuccessfulLogin($user);
+                if ($record_failure) {
+                    $this->handleUnsuccessfulLogin($user);
+                }
                 return [PASSWORD_INCORRECT, $user];      // if the password was incorrect, dont give the md5's a spin
             }
         }
@@ -2034,7 +2040,9 @@ class UsersLib extends TikiLib
             return [USER_VALID, $user];
         }
 
-        $this->handleUnsuccessfulLogin($user);
+        if ($record_failure) {
+            $this->handleUnsuccessfulLogin($user);
+        }
 
         return [PASSWORD_INCORRECT, $user];
     }


=====================================
templates/modules/mod-login_box.tpl
=====================================
@@ -99,12 +99,21 @@ $(document).ready(function () {
 
     function validateUserCredentials(username, password) {
         return new Promise((resolve, reject) => {
+            const $form = $("#loginbox-{{$module_logo_instance}}");
+            const ticket = $form.find("input[name=ticket]").val() || "";
             $.ajax({
                 url: $.service("user", "ValidateUser"),
                 type: 'POST',
-                data: { username: username, password: password },
+                data: { username: username, password: password, ticket: ticket },
                 success: function (res) {
-                    resolve(res);
+                    if (res && typeof res === 'object') {
+                        if (res.ticket) {
+                            $form.find("input[name=ticket]").val(res.ticket);
+                        }
+                        resolve(!!res.valid);
+                    } else {
+                        resolve(!!res);
+                    }
                 },
                 error: function (req, status, error) {
                     displayFeedback("error", error);



View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/a574dd138b29f7b87a3b25001d24be0087c5bbc9

-- 
View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/a574dd138b29f7b87a3b25001d24be0087c5bbc9
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.