Subversion Repositories oidplus

Rev

Rev 1106 | Rev 1116 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
2 daniel-mar 1
<?php
2
 
3
/*
4
 * OIDplus 2.0
1086 daniel-mar 5
 * Copyright 2019 - 2023 Daniel Marschall, ViaThinkSoft
2 daniel-mar 6
 *
7
 * Licensed under the Apache License, Version 2.0 (the "License");
8
 * you may not use this file except in compliance with the License.
9
 * You may obtain a copy of the License at
10
 *
11
 *     http://www.apache.org/licenses/LICENSE-2.0
12
 *
13
 * Unless required by applicable law or agreed to in writing, software
14
 * distributed under the License is distributed on an "AS IS" BASIS,
15
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
 * See the License for the specific language governing permissions and
17
 * limitations under the License.
18
 */
19
 
1050 daniel-mar 20
namespace ViaThinkSoft\OIDplus;
511 daniel-mar 21
 
1086 daniel-mar 22
// phpcs:disable PSR1.Files.SideEffects
23
\defined('INSIDE_OIDPLUS') or die;
24
// phpcs:enable PSR1.Files.SideEffects
25
 
730 daniel-mar 26
class OIDplusAuthUtils extends OIDplusBaseClass {
2 daniel-mar 27
 
566 daniel-mar 28
        // Useful functions
29
 
617 daniel-mar 30
        private static function raPepperProcessing(string $password): string {
31
                // Additional feature: Pepper
32
                // The pepper is stored inside the base configuration file
33
                // It prevents that an attacker with SQL write rights can
34
                // create accounts.
35
                // ATTENTION!!! If a pepper is used, then the
36
                // hashes are bound to that pepper. If you change the pepper,
37
                // then ALL passwords of RAs become INVALID!
38
                $pepper = OIDplus::baseConfig()->getValue('RA_PASSWORD_PEPPER','');
39
                if ($pepper !== '') {
711 daniel-mar 40
                        $algo = OIDplus::baseConfig()->getValue('RA_PASSWORD_PEPPER_ALGO','sha512'); // sha512 works with PHP 7.0
41
                        if (strtolower($algo) === 'sha3-512') {
42
                                $hmac = sha3_512_hmac($password, $pepper);
43
                        } else {
44
                                $hmac = hash_hmac($algo, $password, $pepper);
45
                        }
712 daniel-mar 46
                        if ($hmac === false) throw new OIDplusException(_L('HMAC failed'));
617 daniel-mar 47
                        return $hmac;
48
                } else {
49
                        return $password;
50
                }
51
        }
52
 
585 daniel-mar 53
        // Content provider
577 daniel-mar 54
 
585 daniel-mar 55
        public function getAuthMethod() {
56
                $acs = $this->getAuthContentStore();
57
                if (is_null($acs)) return 'null';
58
                return get_class($acs);
577 daniel-mar 59
        }
60
 
585 daniel-mar 61
        protected function getAuthContentStore() {
62
                // Logged in via JWT
63
                $tmp = OIDplusAuthContentStoreJWT::getActiveProvider();
64
                if ($tmp) return $tmp;
577 daniel-mar 65
 
585 daniel-mar 66
                // Normal login via web-browser
67
                // Cookie will only be created once content is stored
68
                $tmp = OIDplusAuthContentStoreSession::getActiveProvider();
69
                if ($tmp) return $tmp;
577 daniel-mar 70
 
585 daniel-mar 71
                // No active session and no JWT token available. User is not logged in.
72
                return null;
577 daniel-mar 73
        }
74
 
585 daniel-mar 75
        public function getExtendedAttribute($name, $default=NULL) {
76
                $acs = $this->getAuthContentStore();
77
                if (is_null($acs)) return $default;
78
                return $acs->getValue($name, $default);
577 daniel-mar 79
        }
80
 
566 daniel-mar 81
        // RA authentication functions
2 daniel-mar 82
 
566 daniel-mar 83
        public function raLogin($email) {
585 daniel-mar 84
                $acs = $this->getAuthContentStore();
85
                if (is_null($acs)) return;
86
                return $acs->raLogin($email);
566 daniel-mar 87
        }
2 daniel-mar 88
 
566 daniel-mar 89
        public function raLogout($email) {
585 daniel-mar 90
                $acs = $this->getAuthContentStore();
91
                if (is_null($acs)) return;
92
                return $acs->raLogout($email);
2 daniel-mar 93
        }
94
 
566 daniel-mar 95
        public function raCheckPassword($ra_email, $password) {
329 daniel-mar 96
                $ra = new OIDplusRA($ra_email);
453 daniel-mar 97
 
617 daniel-mar 98
                // Get RA info from RA
459 daniel-mar 99
                $authInfo = $ra->getAuthInfo();
590 daniel-mar 100
                if (!$authInfo) return false; // user not found
622 daniel-mar 101
 
617 daniel-mar 102
                // Ask plugins if they can verify this hash
453 daniel-mar 103
                $plugins = OIDplus::getAuthPlugins();
104
                if (count($plugins) == 0) {
105
                        throw new OIDplusException(_L('No RA authentication plugins found'));
106
                }
107
                foreach ($plugins as $plugin) {
617 daniel-mar 108
                        if ($plugin->verify($authInfo, self::raPepperProcessing($password))) return true;
453 daniel-mar 109
                }
110
 
111
                return false;
329 daniel-mar 112
        }
113
 
566 daniel-mar 114
        public function raNumLoggedIn() {
585 daniel-mar 115
                $acs = $this->getAuthContentStore();
116
                if (is_null($acs)) return 0;
117
                return $acs->raNumLoggedIn();
85 daniel-mar 118
        }
119
 
566 daniel-mar 120
        public function loggedInRaList() {
585 daniel-mar 121
                if ($this->forceAllLoggedOut()) {
567 daniel-mar 122
                        return array();
123
                } else {
585 daniel-mar 124
                        $acs = $this->getAuthContentStore();
125
                        if (is_null($acs)) return array();
126
                        return $acs->loggedInRaList();
567 daniel-mar 127
                }
2 daniel-mar 128
        }
129
 
566 daniel-mar 130
        public function isRaLoggedIn($email) {
585 daniel-mar 131
                $acs = $this->getAuthContentStore();
132
                if (is_null($acs)) return false;
133
                return $acs->isRaLoggedIn($email);
2 daniel-mar 134
        }
135
 
585 daniel-mar 136
        // "High level" function including logging and checking for valid JWT alternations
617 daniel-mar 137
        public function raLoginEx($email, $remember_me, $origin='') {
585 daniel-mar 138
                $loginfo = '';
139
                $acs = $this->getAuthContentStore();
140
                if (!is_null($acs)) {
141
                        $acs->raLoginEx($email, $loginfo);
142
                        $acs->activate();
143
                } else {
144
                        if ($remember_me) {
145
                                if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_USER', true)) {
146
                                        throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_USER'));
147
                                }
148
                                $ttl = OIDplus::baseConfig()->getValue('JWT_TTL_LOGIN_USER', 10*365*24*60*60);
149
                                $authSimulation = new OIDplusAuthContentStoreJWT();
150
                                $authSimulation->raLoginEx($email, $loginfo);
151
                                $authSimulation->setValue('oidplus_generator', OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN);
152
                                $authSimulation->setValue('exp', time()+$ttl); // JWT "exp" attribute
153
                                $authSimulation->activate();
154
                        } else {
155
                                $authSimulation = new OIDplusAuthContentStoreSession();
156
                                $authSimulation->raLoginEx($email, $loginfo);
157
                                $authSimulation->activate();
158
                        }
159
                }
160
                $logmsg = "RA '$email' logged in";
161
                if ($origin != '') $logmsg .= " via $origin";
162
                if ($loginfo != '') $logmsg .= " ($loginfo)";
163
                OIDplus::logger()->log("[OK]RA($email)!", $logmsg);
164
        }
165
 
166
        public function raLogoutEx($email) {
167
                $loginfo = '';
168
 
169
                $acs = $this->getAuthContentStore();
170
                if (is_null($acs)) return;
171
                $res = $acs->raLogoutEx($email, $loginfo);
172
 
173
                OIDplus::logger()->log("[OK]RA($email)!", "RA '$email' logged out ($loginfo)");
174
 
175
                if (($this->raNumLoggedIn() == 0) && (!$this->isAdminLoggedIn())) {
176
                        // Nobody logged in anymore. Destroy session cookie to make GDPR people happy
177
                        $acs->destroySession();
178
                } else {
179
                        // Get a new token for the remaining users
180
                        $acs->activate();
181
                }
182
 
183
                return $res;
184
        }
185
 
14 daniel-mar 186
        // Admin authentication functions
2 daniel-mar 187
 
566 daniel-mar 188
        public function adminLogin() {
585 daniel-mar 189
                $acs = $this->getAuthContentStore();
190
                if (is_null($acs)) return;
191
                return $acs->adminLogin();
2 daniel-mar 192
        }
193
 
566 daniel-mar 194
        public function adminLogout() {
585 daniel-mar 195
                $acs = $this->getAuthContentStore();
196
                if (is_null($acs)) return;
197
                return $acs->adminLogout();
2 daniel-mar 198
        }
199
 
566 daniel-mar 200
        public function adminCheckPassword($password) {
609 daniel-mar 201
                $cfgData = OIDplus::baseConfig()->getValue('ADMIN_PASSWORD', '');
202
                if (empty($cfgData)) {
360 daniel-mar 203
                        throw new OIDplusException(_L('No admin password set in %1','userdata/baseconfig/config.inc.php'));
261 daniel-mar 204
                }
622 daniel-mar 205
 
609 daniel-mar 206
                if (!is_array($cfgData)) {
207
                        $passwordDataArray = array($cfgData);
208
                } else {
1106 daniel-mar 209
                        $passwordDataArray = $cfgData; // Multiple Administrator passwords
609 daniel-mar 210
                }
622 daniel-mar 211
 
609 daniel-mar 212
                foreach ($passwordDataArray as $passwordData) {
1107 daniel-mar 213
                        if (str_starts_with($passwordData, '$')) {
214
                                // Version 3: BCrypt (or any other crypt)
215
                                $ok = password_verify($password, $passwordData);
216
                        } else if (strpos($passwordData, '$') !== false) {
217
                                // Version 2: SHA3-512 with salt
218
                                list($salt, $hash) = explode('$', $passwordData, 2);
219
                                $ok = hash_equals(sha3_512($salt.$password, true), base64_decode($hash));
456 daniel-mar 220
                        } else {
609 daniel-mar 221
                                // Version 1: SHA3-512 without salt
1107 daniel-mar 222
                                $ok = hash_equals(sha3_512($password, true), base64_decode($passwordData));
456 daniel-mar 223
                        }
1107 daniel-mar 224
                        if ($ok) return true;
421 daniel-mar 225
                }
609 daniel-mar 226
 
227
                return false;
2 daniel-mar 228
        }
229
 
566 daniel-mar 230
        public function isAdminLoggedIn() {
585 daniel-mar 231
                if ($this->forceAllLoggedOut()) {
567 daniel-mar 232
                        return false;
233
                } else {
585 daniel-mar 234
                        $acs = $this->getAuthContentStore();
235
                        if (is_null($acs)) return false;
236
                        return $acs->isAdminLoggedIn();
567 daniel-mar 237
                }
2 daniel-mar 238
        }
239
 
585 daniel-mar 240
        // "High level" function including logging and checking for valid JWT alternations
617 daniel-mar 241
        public function adminLoginEx($remember_me, $origin='') {
585 daniel-mar 242
                $loginfo = '';
243
                $acs = $this->getAuthContentStore();
244
                if (!is_null($acs)) {
245
                        $acs->adminLoginEx($loginfo);
246
                        $acs->activate();
247
                } else {
248
                        if ($remember_me) {
249
                                if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) {
250
                                        throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_ADMIN'));
251
                                }
252
                                $ttl = OIDplus::baseConfig()->getValue('JWT_TTL_LOGIN_ADMIN', 10*365*24*60*60);
253
                                $authSimulation = new OIDplusAuthContentStoreJWT();
254
                                $authSimulation->adminLoginEx($loginfo);
255
                                $authSimulation->setValue('oidplus_generator', OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN);
256
                                $authSimulation->setValue('exp', time()+$ttl); // JWT "exp" attribute
257
                                $authSimulation->activate();
258
                        } else {
259
                                $authSimulation = new OIDplusAuthContentStoreSession();
260
                                $authSimulation->adminLoginEx($loginfo);
261
                                $authSimulation->activate();
262
                        }
263
                }
264
                $logmsg = "Admin logged in";
265
                if ($origin != '') $logmsg .= " via $origin";
266
                if ($loginfo != '') $logmsg .= " ($loginfo)";
267
                OIDplus::logger()->log("[OK]A!", $logmsg);
268
        }
269
 
270
        public function adminLogoutEx() {
271
                $loginfo = '';
272
 
273
                $acs = $this->getAuthContentStore();
274
                if (is_null($acs)) return;
275
                $res = $acs->adminLogoutEx($loginfo);
276
 
277
                if ($this->raNumLoggedIn() == 0) {
278
                        // Nobody here anymore. Destroy the cookie to make GDPR people happy
279
                        $acs->destroySession();
280
                } else {
281
                        // Get a new token for the remaining users
282
                        $acs->activate();
283
                }
284
 
285
                OIDplus::logger()->log("[OK]A!", "Admin logged out ($loginfo)");
286
                return $res;
287
        }
288
 
310 daniel-mar 289
        // Authentication keys for validating arguments (e.g. sent by mail)
2 daniel-mar 290
 
291
        public static function makeAuthKey($data) {
711 daniel-mar 292
                return sha3_512_hmac($data, 'authkey:'.OIDplus::baseConfig()->getValue('SERVER_SECRET'), false);
2 daniel-mar 293
        }
294
 
295
        public static function validateAuthKey($data, $auth_key) {
617 daniel-mar 296
                return hash_equals(self::makeAuthKey($data), $auth_key);
2 daniel-mar 297
        }
298
 
329 daniel-mar 299
        // "Veto" functions to force logout state
300
 
585 daniel-mar 301
        protected function forceAllLoggedOut() {
329 daniel-mar 302
                if (isset($_SERVER['SCRIPT_FILENAME']) && (basename($_SERVER['SCRIPT_FILENAME']) == 'sitemap.php')) {
303
                        // The sitemap may not contain any confidential information,
304
                        // even if the user is logged in, because the admin could
305
                        // accidentally copy-paste the sitemap to a
306
                        // search engine control panel while they are logged in
307
                        return true;
308
                } else {
309
                        return false;
310
                }
311
        }
427 daniel-mar 312
 
424 daniel-mar 313
        // CSRF functions
427 daniel-mar 314
 
424 daniel-mar 315
        private $enable_csrf = true;
427 daniel-mar 316
 
424 daniel-mar 317
        public function enableCSRF() {
318
                $this->enable_csrf = true;
319
        }
427 daniel-mar 320
 
424 daniel-mar 321
        public function disableCSRF() {
322
                $this->enable_csrf = false;
323
        }
427 daniel-mar 324
 
424 daniel-mar 325
        public function genCSRFToken() {
1098 daniel-mar 326
                return random_bytes_ex(64, false, false);
424 daniel-mar 327
        }
427 daniel-mar 328
 
424 daniel-mar 329
        public function checkCSRF() {
427 daniel-mar 330
                if (!$this->enable_csrf) return;
866 daniel-mar 331
 
332
                $request_token = isset($_REQUEST['csrf_token']) ? $_REQUEST['csrf_token'] : '';
333
                $cookie_token = isset($_COOKIE['csrf_token']) ? $_COOKIE['csrf_token'] : '';
334
 
335
                if (empty($request_token) || empty($cookie_token) || ($request_token !== $cookie_token)) {
336
                        if (OIDplus::baseConfig()->getValue('DEBUG')) {
337
                                throw new OIDplusException(_L('Missing or wrong CSRF Token: Request %1 vs Cookie %2',
338
                                        isset($_REQUEST['csrf_token']) ? '"'.$_REQUEST['csrf_token'].'"' : 'NULL',
339
                                        isset($_COOKIE['csrf_token']) ? $_COOKIE['csrf_token'] : 'NULL'
340
                                ));
341
                        } else {
342
                                throw new OIDplusException(_L('Missing or wrong "CSRF Token". To fix the issue, try clearing your browser cache and reload the page. If you visited the page via HTTPS before, try HTTPS in case you are currently connected via HTTP.'));
343
                        }
424 daniel-mar 344
                }
345
        }
329 daniel-mar 346
 
421 daniel-mar 347
        // Generate RA passwords
622 daniel-mar 348
 
461 daniel-mar 349
        public static function raGeneratePassword($password): OIDplusRAAuthInfo {
1099 daniel-mar 350
                $plugin = OIDplus::getDefaultRaAuthPlugin(true);
1088 daniel-mar 351
                return $plugin->generate(self::raPepperProcessing($password));
421 daniel-mar 352
        }
353
 
354
        // Generate admin password
355
 
585 daniel-mar 356
        /* Nothing here; the admin password will be generated in setup_base.js , purely in the web-browser */
421 daniel-mar 357
 
358
}