PluginProbe ʕ •ᴥ•ʔ
JetBackup – Backup, Restore & Migrate / trunk
JetBackup – Backup, Restore & Migrate vtrunk
3.1.22.3 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.8.1 1.4.9 1.5.0 1.5.1 1.5.1.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.0 1.6.10 1.6.11 1.6.12 1.6.13 1.6.15 1.6.5.1 1.6.8.8 1.6.9 1.6.9.1 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7.5 2.0.8.7 2.0.9.11 2.0.9.14 2.0.9.15 2.0.9.6 2.0.9.7 2.0.9.9 3.1.10.7 3.1.11.1 3.1.12.3 3.1.13.4 3.1.14.17 3.1.15.4 3.1.16.1 3.1.17.5 3.1.18.10 3.1.18.8 3.1.18.9 3.1.19.8 3.1.20.3 3.1.21.3 3.1.7.9 3.1.9.2 trunk 1.1.90 1.1.91 1.2.0 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 1.4.2
backup / src / JetBackup / MFA / GoogleAuthenticator.php
backup / src / JetBackup / MFA Last commit date
.htaccess 1 year ago GoogleAuthenticator.php 7 months ago index.html 1 year ago web.config 1 year ago
GoogleAuthenticator.php
166 lines
1 <?php
2
3 namespace JetBackup\MFA;
4
5 use Exception;
6 use JetBackup\Wordpress\Helper;
7 use JetBackup\Wordpress\Wordpress;
8 use SimpleThenticator\SimpleAuthenticator;
9
10 if (!defined('__JETBACKUP__')) die('Direct access is not allowed');
11
12 class GoogleAuthenticator {
13
14 const MFA_KEY = 'jetbackup_mfa_google_authenticator';
15 const MFA_COOKIE_KEY = 'jetbackup_mfa_auth';
16 const MFA_SETUP_COMPLETED = 'jetbackup_mfa_setup_completed';
17
18 const MFA_MAX_ATTEMPTS = 10;
19 const MFA_MAX_ATTEMPTS_KEY = 'jetbackup_mfa_max_attempts';
20
21 private static ?SimpleAuthenticator $authenticator = null;
22
23 private static function getAuthenticator(): SimpleAuthenticator {
24 if (self::$authenticator === null) {
25 self::$authenticator = new SimpleAuthenticator();
26 }
27 return self::$authenticator;
28 }
29
30 /**
31 * Create a secret and store it for the given user ID.
32 *
33 * @throws Exception
34 */
35 public static function createSecret(): string {
36
37 $userId = Helper::getUserId();
38 $secret = self::getAuthenticator()::createSecret();
39 if (!Wordpress::updateUserMeta($userId, self::MFA_KEY, $secret, '')) {
40 throw new Exception('Failed to save MFA secret for user ID: ' . $userId);
41 }
42
43 return $secret;
44 }
45
46 public static function getCookieHash(): string {
47 return hash_hmac('sha512', Wordpress::getAuthSalt(), Wordpress::getAuthKey());
48 }
49
50 public static function setCookie(): void {
51 $expire = time() + 86400;
52 $path = defined('COOKIEPATH') ? COOKIEPATH : '/';
53 $domain = defined('COOKIE_DOMAIN') ? COOKIE_DOMAIN : '';
54 $secure = is_ssl();
55
56 setcookie(
57 self::MFA_COOKIE_KEY,
58 self::getCookieHash(),
59 [
60 'expires' => $expire,
61 'path' => $path,
62 'domain' => $domain,
63 'secure' => $secure,
64 'httponly' => true,
65 'samesite' => 'None'
66 ]
67 );
68 }
69
70 /**
71 * Clear the MFA setup for a user.
72 */
73 public static function clearCookie(): void {
74 $userId = Helper::getUserId();
75
76 $expire = time() - 3600;
77 $path = defined('COOKIEPATH') ? COOKIEPATH : '/';
78 $domain = defined('COOKIE_DOMAIN') ? COOKIE_DOMAIN : '';
79 $secure = is_ssl();
80
81 setcookie(
82 self::MFA_COOKIE_KEY,
83 '',
84 [
85 'expires' => $expire,
86 'path' => $path,
87 'domain' => $domain,
88 'secure' => $secure,
89 'httponly' => true,
90 'samesite' => 'None'
91 ]
92 );
93
94 // Clear user metadata for MFA
95 Wordpress::deleteUserMeta($userId, self::MFA_KEY);
96 Wordpress::deleteUserMeta($userId, self::MFA_SETUP_COMPLETED);
97 }
98
99
100 public static function isSetupCompleted(): bool {
101 $userId = Helper::getUserId();
102 return Wordpress::getUserMeta($userId, self::MFA_SETUP_COMPLETED, true) ?? false;
103 }
104
105 /**
106 * Generate a QR Code URL for Google Authenticator.
107 *
108 * @return array
109 * @throws Exception
110 */
111 public static function getQRcode(): array {
112
113 $authenticator = self::getAuthenticator();
114 $domain = Wordpress::getSiteDomain();
115 $label = "JetBackup [$domain]";
116 $userId = Helper::getUserId();
117 $secret = Wordpress::getUserMeta($userId, self::MFA_KEY, true);
118 if (!$secret || $secret == '') $secret = self::createSecret();
119
120 $setupCompleted = self::isSetupCompleted();
121 return [
122 'code' => $setupCompleted ? '' : $authenticator->getQRCodeGoogleUrl($secret, $label),
123 'isFirstTime' => !$setupCompleted,
124 ];
125
126 }
127
128 /**
129 * Verify the provided MFA code.
130 *
131 * @param int $code
132 *
133 * @return bool
134 */
135 public static function verifyCode(int $code): bool {
136 $userId = Helper::getUserId();
137 $secret = Wordpress::getUserMeta($userId, self::MFA_KEY, true);
138 if (!$secret) return false;
139
140 $attempts = (int) Wordpress::getUserMeta($userId, self::MFA_MAX_ATTEMPTS_KEY, true);
141
142 if ($attempts > 0) usleep(min(pow($attempts, 2) * 100000, 3000000)); // up to 3s
143 $isValid = self::getAuthenticator()->verifyCode($secret, $code, 3, null);
144
145 if ($isValid) {
146 Wordpress::updateUserMeta($userId, self::MFA_SETUP_COMPLETED, 'true', '');
147 Wordpress::deleteUserMeta($userId, self::MFA_MAX_ATTEMPTS_KEY);
148 return true;
149 }
150
151 // Failed attempt
152 $attempts++;
153 Wordpress::updateUserMeta($userId, self::MFA_MAX_ATTEMPTS_KEY, $attempts, '');
154
155 // If max attempts reached, logout
156 if ($attempts >= self::MFA_MAX_ATTEMPTS) {
157 Wordpress::wpLogout();
158 Wordpress::wpRedirect(Wordpress::wpLoginURL('', true));
159 exit;
160 }
161
162 return false;
163 }
164
165
166 }