[TikiWiki-commits] [Git][tikiwiki/tiki][master] [NEW] 2FA: Apply a grace period per user and per group for enforced 2FA method subjects

"Merci Jacob \(@mercihabam\) via TikiWiki-cvs" <[email protected]>
Newsgroups gmane.comp.cms.tiki.cvs
Message-ID <68fcc0cf837e1_2ce1b044768a@gitlab-sidekiq-low-urgency-cpu-bound-v2-854849f664-whvp4.mail>

Merci Jacob pushed to branch master at Tiki Wiki CMS Groupware / Tiki


Commits:
a4dad2b1 by Merci Jacob at 2025-10-25T12:13:54+00:00
[NEW] 2FA: Apply a grace period per user and per group for enforced 2FA method subjects
---
* reset user grace period start only when a grace period is given

* fix db

* add per-user grace period

* fix php linter

* [NEW] 2FA: Apply a grace period per user and per group for enforced 2FA method subjects

See merge request tikiwiki/tiki!8710

- - - - -


12 changed files:

- db/tiki.sql
- + installer/schema/20251003_2fa_grace_period_tiki.sql
- lib/core/Services/Group/Controller.php
- lib/core/Tiki/Profile/Installer.php
- lib/prefs/global.php
- lib/userslib.php
- templates/admin/include_login.tpl
- templates/tiki-admingroups.tpl
- templates/tiki-adminusers.tpl
- tiki-admingroups.php
- tiki-adminusers.php
- tiki-setup.php


Changes:

=====================================
db/tiki.sql
=====================================
@@ -2944,6 +2944,7 @@ CREATE TABLE `users_groups` (
   `prorateInterval` varchar(255) default '',
   `isRole` char(1) DEFAULT 'n',
   `isTplGroup` char(1) DEFAULT 'n',
+  `twoFactorAuthGracePeriod` int(11) DEFAULT NULL,
   PRIMARY KEY (`id`),
   UNIQUE KEY `groupName` (`groupName` (191)),
   KEY `expireAfter` (`expireAfter`)
@@ -3006,6 +3007,8 @@ CREATE TABLE `users_users` (
   `unsuccessful_logins` int(14) default 0,
   `waiting` char(1) default NULL,
   `twoFactorSecret` varchar(32) default NULL,
+  `twoFactorAuthGracePeriod` int(11) DEFAULT NULL,
+  `twoFactorGracePeriodStart` int(14) DEFAULT NULL,
   `last_mfa_date` bigint DEFAULT NULL,
   PRIMARY KEY (`userId`),
   UNIQUE KEY `login` (login (191)),


=====================================
installer/schema/20251003_2fa_grace_period_tiki.sql
=====================================
@@ -0,0 +1,5 @@
+ALTER TABLE `users_users`
+ADD `twoFactorAuthGracePeriod` INT(11) DEFAULT NULL AFTER `twoFactorSecret`,
+ADD `twoFactorGracePeriodStart` INT(14) DEFAULT NULL AFTER `twoFactorAuthGracePeriod`;
+
+ALTER TABLE `users_groups` ADD `twoFactorAuthGracePeriod` INT(11) DEFAULT NULL AFTER `isTplGroup`;


=====================================
lib/core/Services/Group/Controller.php
=====================================
@@ -373,7 +373,8 @@ class Services_Group_Controller
                 $params['color'],
                 $params['isRole'],
                 $params['isTplGroup'],
-                $params['include_groups'] ?? []
+                $params['include_groups'] ?? [],
+                $params['twoFactorAuthGracePeriod']
             );
 
 
@@ -723,7 +724,7 @@ class Services_Group_Controller
             'groupfield' => 0,
             'userstracker' => 0,
             'usersfield' => 0,
-            'registrationUsersFieldIds' => ''
+            'registrationUsersFieldIds' => '',
         ];
         global $prefs;
         $prefGroupTracker = isset($prefs['groupTracker']) and $prefs['groupTracker'] == 'y';


=====================================
lib/core/Tiki/Profile/Installer.php
=====================================
@@ -634,7 +634,8 @@ class Tiki_Profile_Installer
                             $info['emailPattern'],
                             $info['anniversary'],
                             $info['prorateInterval'],
-                            $info['groupColor']
+                            $info['groupColor'],
+                            twaFAGracePeriod: $info['twoFactorAuthGracePeriod']
                         );
                         $this->setFeedback(tra('Group modified') . ': ' . $info['groupName']);
                     }
@@ -808,7 +809,8 @@ class Tiki_Profile_Installer
                     $info['email_pattern'],
                     $info['anniversary'],
                     $info['prorate_interval'],
-                    $info['color']
+                    $info['color'],
+                    twaFAGracePeriod: $info['twoFactorAuthGracePeriod']
                 );
             }
         }


=====================================
lib/prefs/global.php
=====================================
@@ -259,6 +259,15 @@ function prefs_global_list($partial = false)
             ],
             'default' => 'n',
         ],
+        'twoFactorAuthGracePeriod' => [
+            'name' => tra('2FA Grace Period'),
+            'description' => tra('Number of days to allow users to access the site without 2FA before forcing them to set it up. Note: this applies globally. If you want specific periods per groups, visit the groups settings.'),
+            'type' => 'text',
+            'default' => '0',
+            'dependencies' => [
+                'twoFactorAuth',
+            ],
+        ],
         'twoFactorAuthIncludedGroup' => [
             'name' => tra('Force users in the indicated groups to enable 2FA'),
             'description' => tra('List of group names.'),


=====================================
lib/userslib.php
=====================================
@@ -7514,6 +7514,51 @@ class UsersLib extends TikiLib
         return $this->getOne($query, [$user]);
     }
 
+    public function get2FAGracePeriod($user)
+    {
+        global $prefs;
+
+        $userInfo = $this->get_user_info($user);
+        if ($userInfo['twoFactorAuthGracePeriod'] !== null) {
+            return (int)$userInfo['twoFactorAuthGracePeriod'];
+        }
+
+        $userGroups = $this->get_user_groups($user);
+        $groupsGracePeriods = array_filter(array_map(function ($group) {
+            return $this->get_group_info($group)['twoFactorAuthGracePeriod'];
+        }, $userGroups), function ($value) {
+            return $value !== null;
+        });
+        $maxGroupGracePeriod = ! empty($groupsGracePeriods) ? max($groupsGracePeriods) : null;
+
+        return (int)($maxGroupGracePeriod !== null ? $maxGroupGracePeriod : $prefs['twoFactorAuthGracePeriod']);
+    }
+
+    public function get2FAGracePeriodStart($user)
+    {
+        $query = 'select `twoFactorGracePeriodStart` from `users_users` where `login`=?';
+        return $this->getOne($query, [$user]);
+    }
+
+    public function reset2FAGracePeriodStart($user)
+    {
+        $query = 'update `users_users` set `twoFactorGracePeriodStart`=? where binary `login`=?';
+        $now = time();
+        $this->query($query, [$now, $user]);
+        return $now;
+    }
+
+    public function setTwoFactorAuthGracePeriod($user, $gracePeriod)
+    {
+        if ($gracePeriod === '') {
+            $gracePeriod = null;
+        }
+
+        $query = 'update `users_users` set `twoFactorAuthGracePeriod`=? where binary `login`=?';
+        $this->query($query, [$gracePeriod, $user]);
+        return $gracePeriod;
+    }
+
     public function validate_two_factor($twoFactorSecret, $pin, $user)
     {
         $google2fa = new Google2FA();
@@ -7639,7 +7684,8 @@ class UsersLib extends TikiLib
         $color = '',
         $isRole = '',
         $isTplGroup = '',
-        $include_groups = []
+        $include_groups = [],
+        $twaFAGracePeriod = null
     ) {
 
         $tikilib = TikiLib::lib('tiki');
@@ -7669,6 +7715,7 @@ class UsersLib extends TikiLib
             'prorateInterval' => $prorateInterval,
             'isRole' => $isRole,
             'isTplGroup' => empty($isTplGroup) ? 'n' : $isTplGroup,
+            'twoFactorAuthGracePeriod' => $twaFAGracePeriod === '' ? null : $twaFAGracePeriod,
         ];
 
         $id = $this->table('users_groups')->insert($data);
@@ -7709,7 +7756,8 @@ class UsersLib extends TikiLib
         $color = '',
         $isRole = '',
         $isTplGroup = '',
-        $include_groups = []
+        $include_groups = [],
+        $twaFAGracePeriod = null
     ) {
         $isTplGroup = empty($isTplGroup) ? 'n' : $isTplGroup;
         $users = $this->get_group_users($group);
@@ -7742,7 +7790,9 @@ class UsersLib extends TikiLib
                 $prorateInterval,
                 $color,
                 $isRole,
-                $isTplGroup
+                $isTplGroup,
+                [],
+                $twaFAGracePeriod
             );
         }
 
@@ -7770,6 +7820,7 @@ class UsersLib extends TikiLib
             'prorateInterval' => $prorateInterval,
             'isRole' => $isRole,
             'isTplGroup' => $isTplGroup,
+            'twoFactorAuthGracePeriod' => $twaFAGracePeriod === '' ? null : $twaFAGracePeriod,
         ];
 
         $this->table('users_groups')->update($data, ['groupName' => $olgroup]);
@@ -7883,7 +7934,8 @@ class UsersLib extends TikiLib
             $groupInfo["groupColor"],
             $groupInfo["isRole"],
             $groupInfo["isTplGroup"],
-            $includeGroups
+            $includeGroups,
+            $groupInfo["twoFactorAuthGracePeriod"]
         );
         return true;
     }


=====================================
templates/admin/include_login.tpl
=====================================
@@ -227,6 +227,7 @@
                     {preference name=twoFactorAuthEmailTokenChars}
                     {preference name=twoFactorAuthEmailTokenTTL}
                     {preference name=twoFactorAuthAllUsers}
+                    {preference name=twoFactorAuthGracePeriod}
                     {preference name=twoFactorAuthIncludedGroup}
                     {preference name=twoFactorAuthIncludedUsers}
                     {preference name=twoFactorAuthExcludedGroup}


=====================================
templates/tiki-admingroups.tpl
=====================================
@@ -417,6 +417,17 @@
                         </div>
                     </div>
                 {/if}
+                {if $prefs.twoFactorAuth eq 'y'}
+                    <div class="mb-3 row">
+                        <label class="col-form-label col-md-3">{tr}2FA Grace Period{/tr}</label>
+                        <div class="col-md-9">
+                            <input type="number" class="form-control" name="twoFactorAuthGracePeriod" value="{$twoFactorAuthGracePeriod|escape}">
+                            <div class="form-text">
+                                {tr}Number of days to allow users in this group to access the site without 2FA before forcing them to set it up.{/tr}
+                            </div>
+                        </div>
+                    </div>
+                {/if}
                 {if $groupname neq 'Anonymous' and $groupname neq 'Registered' and $groupname neq 'Admins'}
                     <div class="mb-3 row">
                         <label class="col-form-label col-md-3">{tr}User Choice{/tr}</label>


=====================================
templates/tiki-adminusers.tpl
=====================================
@@ -613,6 +613,19 @@
                                 <small class="form-text text-muted">{tr}This will require the user to reset up 2FA the next time they log in.{/tr}</small>
                             </div>
                         </div>
+
+                        {if $force2FA}
+                            <div class="mb-3 row">
+                                <label class="col-form-label col-md-2">{tr}2FA Grace Period{/tr}</label>
+                                <div class="col-md-6">
+                                    <input type="number" class="form-control" name="twoFactorAuthGracePeriod" value="{$userinfo.twoFactorAuthGracePeriod|escape}">
+                                    <div class="form-text">
+                                        {tr}Number of days to allow this user to access the site without 2FA before forcing them to set it up.{/tr}
+                                    </div>
+                                </div>
+                            </div>
+                        {/if}
+
                     {/if}
 
                     <div class="mb-3 row">


=====================================
tiki-admingroups.php
=====================================
@@ -241,6 +241,10 @@ if (! empty($_REQUEST["group"])) {
             }
         }
     }
+
+    if ($prefs['twoFactorAuth'] == 'y') {
+        $smarty->assign('twoFactorAuthGracePeriod', $re['twoFactorAuthGracePeriod']);
+    }
     $groupperms = $re["perms"];
     //$allgroups = $userlib->list_all_groups();
     $allgroups = $userlib->list_can_include_groups($re["groupName"]);


=====================================
tiki-adminusers.php
=====================================
@@ -555,6 +555,11 @@ if (isset($_REQUEST['user']) and $_REQUEST['user']) {
             $userlib->send_validation_email($_POST['login'], $userinfo['valid'], $_POST['email'], 'y');
         }
 
+        if (isset($_POST['twoFactorAuthGracePeriod']) && $userinfo['twoFactorAuthGracePeriod'] != $_POST['twoFactorAuthGracePeriod']) {
+            $userlib->setTwoFactorAuthGracePeriod($userinfo['login'], $_POST['twoFactorAuthGracePeriod']);
+            $userlib->reset2FAGracePeriodStart($userinfo['login']);
+        }
+
         $cookietab = '1';
     }
 


=====================================
tiki-setup.php
=====================================
@@ -128,14 +128,32 @@ $twoFactorSecret = $userlib->get_2_factor_secret($user);
 $force2FA = $userlib->forceTwoFactorAuth($user);
 //Check if 2FA is required for a user and if it not yet enabled
 if ($prefs['twoFactorAuth'] == 'y' && empty($twoFactorSecret) && $force2FA && ! empty($user)) {
-    $accesslib = TikiLib::lib('access');
-    //URL to send user who has not yet enabled 2FA
-    $accessibleUrl = $base_url . 'tiki-user_preferences.php';
-    $pageUrl = $url_scheme . "://" . $host . $requestUri;
-    //Do not redirect the user if it is a logout action
-    if ($accessibleUrl !== $pageUrl && $pageUrl !== $base_url . 'tiki-logout.php') {
-        header('location: tiki-user_preferences.php');
-        exit();
+    $gracePeriod = $userlib->get2FAGracePeriod($user);
+    $userGracePeriodStart = $userlib->get2FAGracePeriodStart($user);
+
+    if (! $userGracePeriodStart) {
+        if ($gracePeriod > 0) {
+            $userGracePeriodStart = $userlib->reset2FAGracePeriodStart($user);
+        } else {
+            $userGracePeriodStart = 0;
+        }
+    }
+
+    $userGracePeriodShouldEnd = $userGracePeriodStart + ($gracePeriod * 24 * 3600);
+
+    if ($userGracePeriodShouldEnd < time()) {
+        $accesslib = TikiLib::lib('access');
+        //URL to send user who has not yet enabled 2FA
+        $accessibleUrl = $base_url . 'tiki-user_preferences.php';
+        $pageUrl = $url_scheme . "://" . $host . $requestUri;
+        //Do not redirect the user if it is a logout action
+        if ($accessibleUrl !== $pageUrl && $pageUrl !== $base_url . 'tiki-logout.php') {
+            header('location: tiki-user_preferences.php');
+            exit();
+        }
+    } else {
+        $daysLeft = ceil(($userGracePeriodShouldEnd - time()) / (24 * 3600));
+        Feedback::warning(tra(strtr('You must enable Two-Factor Authentication within the next %days% day(s) to continue using your account. Please update your <a href="%url%">user preferences</a>.', ['%days%' => $daysLeft, '%url%' => 'tiki-user_preferences.php'])));
     }
 }
 



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

-- 
View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/a4dad2b1e820fa7df8061cf6a3d77e5fede46021
You're receiving this email because of your account on gitlab.com.

_______________________________________________
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.