• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

daycry / auth / 22545351205

01 Mar 2026 02:24PM UTC coverage: 63.668% (+0.4%) from 63.267%
22545351205

push

github

daycry
Make TOTP 2FA optional; add tests and session cleanup

Totp2FA no longer forces TOTP on users who haven't completed setup: createIdentity() now returns an empty string to skip the action for users without a confirmed TOTP, and returns 'totp' and inserts a pending marker for confirmed users. Stale pending markers are removed before inserting new ones. Session authenticator now clears auth_action/auth_action_message in completeLogin() and respects an empty secret from startUpAction() (treating the action as skipped). HasTotp() now only treats a confirmed identity (name='totp') as enabled, excluding unverified enrollment. UserSecurityController view calls were switched to use configurable view keys and a new security_overview view key was added to Auth config. Added comprehensive Totp2FATest unit tests and updated phpstan baseline to suppress a test-specific static method warning.

12 of 15 new or added lines in 4 files covered. (80.0%)

3086 of 4847 relevant lines covered (63.67%)

43.0 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/Controllers/UserSecurityController.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of Daycry Auth.
7
 *
8
 * (c) Daycry <daycry9@proton.me>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace Daycry\Auth\Controllers;
15

16
use CodeIgniter\HTTP\RedirectResponse;
17
use Daycry\Auth\Libraries\TOTP;
18
use Daycry\Auth\Models\DeviceSessionModel;
19

20
/**
21
 * UserSecurityController
22
 *
23
 * Lets authenticated users manage their own security settings:
24
 *   - Active device sessions (list + revoke)
25
 *   - TOTP 2FA setup / disable
26
 *
27
 * Routes to add in your app (example):
28
 *   $routes->group('account/security', ['filter' => 'session'], static function ($routes): void {
29
 *       $routes->get('/',                  'Daycry\Auth\Controllers\UserSecurityController::index',             ['as' => 'security']);
30
 *       $routes->post('sessions/revoke',   'Daycry\Auth\Controllers\UserSecurityController::revokeSession',     ['as' => 'security-revoke-session']);
31
 *       $routes->post('sessions/revoke-all','Daycry\Auth\Controllers\UserSecurityController::revokeAllSessions',['as' => 'security-revoke-all']);
32
 *       $routes->get('totp/setup',         'Daycry\Auth\Controllers\UserSecurityController::totpSetup',        ['as' => 'totp-setup']);
33
 *       $routes->post('totp/setup/confirm','Daycry\Auth\Controllers\UserSecurityController::totpSetupConfirm', ['as' => 'totp-setup-confirm']);
34
 *       $routes->post('totp/disable',      'Daycry\Auth\Controllers\UserSecurityController::totpDisable',      ['as' => 'totp-disable']);
35
 *       $routes->post('sessions/password', 'Daycry\Auth\Controllers\UserSecurityController::changePassword',   ['as' => 'security-change-password']);
36
 *       $routes->get('email/change',       'Daycry\Auth\Controllers\UserSecurityController::changeEmailView',  ['as' => 'security-change-email']);
37
 *       $routes->post('email/change',      'Daycry\Auth\Controllers\UserSecurityController::changeEmail',      ['as' => 'security-change-email-action']);
38
 *       $routes->get('email/confirm',      'Daycry\Auth\Controllers\UserSecurityController::confirmEmailChange',['as' => 'security-confirm-email']);
39
 *       $routes->post('oauth/unlink',      'Daycry\Auth\Controllers\UserSecurityController::unlinkOauth',      ['as' => 'security-unlink-oauth']);
40
 *   });
41
 */
42
class UserSecurityController extends BaseAuthController
43
{
44
    protected function getValidationRules(): array
×
45
    {
46
        return [];
×
47
    }
48

49
    /**
50
     * Security overview: device sessions + TOTP status.
51
     */
52
    public function index(): string
×
53
    {
54
        $user = auth()->user();
×
55

56
        /** @var DeviceSessionModel $deviceModel */
57
        $deviceModel = model(DeviceSessionModel::class);
×
58

59
        $sessions   = $deviceModel->getActiveForUser($user);
×
60
        $currentSid = session_id();
×
61

NEW
62
        return $this->view(setting('Auth.views')['security_overview'], [
×
63
            'sessions'    => $sessions,
×
64
            'currentSid'  => $currentSid,
×
65
            'totpEnabled' => $user->hasTotpEnabled(),
×
66
        ]);
×
67
    }
68

69
    /**
70
     * Revoke a single device session.
71
     */
72
    public function revokeSession(): RedirectResponse
×
73
    {
74
        $sessionId = (string) $this->request->getPost('session_id');
×
75

76
        if ($sessionId === '') {
×
77
            return redirect()->back()->with('error', 'Invalid session.');
×
78
        }
79

80
        $user = auth()->user();
×
81

82
        /** @var DeviceSessionModel $deviceModel */
83
        $deviceModel = model(DeviceSessionModel::class);
×
84

85
        // Safety check: ensure the session belongs to the current user
86
        $session = $deviceModel->findBySessionId($sessionId);
×
87

88
        if ($session === null || $session->user_id !== $user->id) {
×
89
            return redirect()->back()->with('error', 'Session not found.');
×
90
        }
91

92
        $deviceModel->terminateSession($sessionId);
×
93

94
        return redirect()->back()->with('message', 'Session revoked successfully.');
×
95
    }
96

97
    /**
98
     * Revoke all other active sessions (keep current one).
99
     */
100
    public function revokeAllSessions(): RedirectResponse
×
101
    {
102
        $user = auth()->user();
×
103

104
        /** @var DeviceSessionModel $deviceModel */
105
        $deviceModel = model(DeviceSessionModel::class);
×
106
        $deviceModel->terminateAllForUser($user, session_id());
×
107

108
        return redirect()->back()->with('message', 'All other sessions have been revoked.');
×
109
    }
110

111
    /**
112
     * Show the TOTP setup page with QR code.
113
     */
114
    public function totpSetup(): RedirectResponse|string
×
115
    {
116
        $user = auth()->user();
×
117

118
        if ($user->hasTotpEnabled()) {
×
119
            return redirect()->route('security')->with('error', 'TOTP is already enabled.');
×
120
        }
121

122
        $otpAuthUrl = $user->enableTotp();
×
123
        $secret     = $user->getTotpSecret();
×
124

NEW
125
        return $this->view(setting('Auth.views')['action_totp_setup_show'], [
×
126
            'otpAuthUrl' => $otpAuthUrl,
×
127
            'secret'     => $secret ?? '',
×
128
            'qrCodeUrl'  => TOTP::getQRCodeUrl($otpAuthUrl),
×
129
            'confirmUrl' => url_to('totp-setup-confirm'),
×
130
        ]);
×
131
    }
132

133
    /**
134
     * Confirm TOTP setup by verifying the first code.
135
     */
136
    public function totpSetupConfirm(): RedirectResponse|string
×
137
    {
138
        $user = auth()->user();
×
139
        $code = (string) $this->request->getPost('token');
×
140

141
        if (! $user->verifyTotpCode($code)) {
×
142
            // Remove the just-generated secret so setup can restart cleanly
143
            $user->disableTotp();
×
144

145
            return redirect()->route('totp-setup')
×
146
                ->with('error', lang('Auth.totpSetupInvalidCode'));
×
147
        }
148

NEW
149
        return $this->view(setting('Auth.views')['action_totp_setup_success'], [
×
150
            'redirectUrl' => url_to('security'),
×
151
        ]);
×
152
    }
153

154
    /**
155
     * Disable TOTP for the current user.
156
     */
157
    public function totpDisable(): RedirectResponse
×
158
    {
159
        $code = (string) $this->request->getPost('token');
×
160
        $user = auth()->user();
×
161

162
        if (! $user->verifyTotpCode($code)) {
×
163
            return redirect()->back()->with('error', lang('Auth.invalidTotpToken'));
×
164
        }
165

166
        $user->disableTotp();
×
167

168
        return redirect()->route('security')->with('message', 'Two-factor authentication has been disabled.');
×
169
    }
170
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc