PluginProbe ʕ •ᴥ•ʔ
JetBackup – Backup, Restore & Migrate / 3.1.13.4
JetBackup – Backup, Restore & Migrate v3.1.13.4
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 9 months ago GoogleAuthenticator.php 9 months ago index.html 9 months ago web.config 9 months ago
GoogleAuthenticator.php
165 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 return [
121 'code' => $authenticator->getQRCodeGoogleUrl($secret, $label),
122 'isFirstTime' => !self::isSetupCompleted(),
123 ];
124
125 }
126
127 /**
128 * Verify the provided MFA code.
129 *
130 * @param int $code
131 *
132 * @return bool
133 */
134 public static function verifyCode(int $code): bool {
135 $userId = Helper::getUserId();
136 $secret = Wordpress::getUserMeta($userId, self::MFA_KEY, true);
137 if (!$secret) return false;
138
139 $attempts = (int) Wordpress::getUserMeta($userId, self::MFA_MAX_ATTEMPTS_KEY, true);
140
141 if ($attempts > 0) usleep(min(pow($attempts, 2) * 100000, 3000000)); // up to 3s
142 $isValid = self::getAuthenticator()->verifyCode($secret, $code, 3, null);
143
144 if ($isValid) {
145 Wordpress::updateUserMeta($userId, self::MFA_SETUP_COMPLETED, 'true', '');
146 Wordpress::deleteUserMeta($userId, self::MFA_MAX_ATTEMPTS_KEY);
147 return true;
148 }
149
150 // Failed attempt
151 $attempts++;
152 Wordpress::updateUserMeta($userId, self::MFA_MAX_ATTEMPTS_KEY, $attempts, '');
153
154 // If max attempts reached, logout
155 if ($attempts >= self::MFA_MAX_ATTEMPTS) {
156 Wordpress::wpLogout();
157 Wordpress::wpRedirect(Wordpress::wpLoginURL('', true));
158 exit;
159 }
160
161 return false;
162 }
163
164
165 }