Subversion Repositories oidplus

Rev

Rev 1050 | Rev 1088 | 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
 
457 daniel-mar 30
        public static function getRandomBytes($len) {
31
                if (function_exists('openssl_random_pseudo_bytes')) {
32
                        $a = openssl_random_pseudo_bytes($len);
33
                        if ($a) return $a;
34
                }
35
 
465 daniel-mar 36
                if (function_exists('mcrypt_create_iv')) {
849 daniel-mar 37
                        $a = bin2hex(mcrypt_create_iv($len));
465 daniel-mar 38
                        if ($a) return $a;
39
                }
40
 
457 daniel-mar 41
                if (function_exists('random_bytes')) {
42
                        $a = random_bytes($len);
43
                        if ($a) return $a;
44
                }
45
 
46
                // Fallback to non-secure RNG
47
                $a = '';
48
                while (strlen($a) < $len*2) {
592 daniel-mar 49
                        $a .= sha1(uniqid((string)mt_rand(), true));
457 daniel-mar 50
                }
51
                $a = substr($a, 0, $len*2);
52
                return hex2bin($a);
53
        }
54
 
617 daniel-mar 55
        private static function raPepperProcessing(string $password): string {
56
                // Additional feature: Pepper
57
                // The pepper is stored inside the base configuration file
58
                // It prevents that an attacker with SQL write rights can
59
                // create accounts.
60
                // ATTENTION!!! If a pepper is used, then the
61
                // hashes are bound to that pepper. If you change the pepper,
62
                // then ALL passwords of RAs become INVALID!
63
                $pepper = OIDplus::baseConfig()->getValue('RA_PASSWORD_PEPPER','');
64
                if ($pepper !== '') {
711 daniel-mar 65
                        $algo = OIDplus::baseConfig()->getValue('RA_PASSWORD_PEPPER_ALGO','sha512'); // sha512 works with PHP 7.0
66
                        if (strtolower($algo) === 'sha3-512') {
67
                                $hmac = sha3_512_hmac($password, $pepper);
68
                        } else {
69
                                $hmac = hash_hmac($algo, $password, $pepper);
70
                        }
712 daniel-mar 71
                        if ($hmac === false) throw new OIDplusException(_L('HMAC failed'));
617 daniel-mar 72
                        return $hmac;
73
                } else {
74
                        return $password;
75
                }
76
        }
77
 
585 daniel-mar 78
        // Content provider
577 daniel-mar 79
 
585 daniel-mar 80
        public function getAuthMethod() {
81
                $acs = $this->getAuthContentStore();
82
                if (is_null($acs)) return 'null';
83
                return get_class($acs);
577 daniel-mar 84
        }
85
 
585 daniel-mar 86
        protected function getAuthContentStore() {
87
                // Logged in via JWT
88
                $tmp = OIDplusAuthContentStoreJWT::getActiveProvider();
89
                if ($tmp) return $tmp;
577 daniel-mar 90
 
585 daniel-mar 91
                // Normal login via web-browser
92
                // Cookie will only be created once content is stored
93
                $tmp = OIDplusAuthContentStoreSession::getActiveProvider();
94
                if ($tmp) return $tmp;
577 daniel-mar 95
 
585 daniel-mar 96
                // No active session and no JWT token available. User is not logged in.
97
                return null;
577 daniel-mar 98
        }
99
 
585 daniel-mar 100
        public function getExtendedAttribute($name, $default=NULL) {
101
                $acs = $this->getAuthContentStore();
102
                if (is_null($acs)) return $default;
103
                return $acs->getValue($name, $default);
577 daniel-mar 104
        }
105
 
566 daniel-mar 106
        // RA authentication functions
2 daniel-mar 107
 
566 daniel-mar 108
        public function raLogin($email) {
585 daniel-mar 109
                $acs = $this->getAuthContentStore();
110
                if (is_null($acs)) return;
111
                return $acs->raLogin($email);
566 daniel-mar 112
        }
2 daniel-mar 113
 
566 daniel-mar 114
        public function raLogout($email) {
585 daniel-mar 115
                $acs = $this->getAuthContentStore();
116
                if (is_null($acs)) return;
117
                return $acs->raLogout($email);
2 daniel-mar 118
        }
119
 
566 daniel-mar 120
        public function raCheckPassword($ra_email, $password) {
329 daniel-mar 121
                $ra = new OIDplusRA($ra_email);
453 daniel-mar 122
 
617 daniel-mar 123
                // Get RA info from RA
459 daniel-mar 124
                $authInfo = $ra->getAuthInfo();
590 daniel-mar 125
                if (!$authInfo) return false; // user not found
622 daniel-mar 126
 
617 daniel-mar 127
                // Ask plugins if they can verify this hash
453 daniel-mar 128
                $plugins = OIDplus::getAuthPlugins();
129
                if (count($plugins) == 0) {
130
                        throw new OIDplusException(_L('No RA authentication plugins found'));
131
                }
132
                foreach ($plugins as $plugin) {
617 daniel-mar 133
                        if ($plugin->verify($authInfo, self::raPepperProcessing($password))) return true;
453 daniel-mar 134
                }
135
 
136
                return false;
329 daniel-mar 137
        }
138
 
566 daniel-mar 139
        public function raNumLoggedIn() {
585 daniel-mar 140
                $acs = $this->getAuthContentStore();
141
                if (is_null($acs)) return 0;
142
                return $acs->raNumLoggedIn();
85 daniel-mar 143
        }
144
 
566 daniel-mar 145
        public function loggedInRaList() {
585 daniel-mar 146
                if ($this->forceAllLoggedOut()) {
567 daniel-mar 147
                        return array();
148
                } else {
585 daniel-mar 149
                        $acs = $this->getAuthContentStore();
150
                        if (is_null($acs)) return array();
151
                        return $acs->loggedInRaList();
567 daniel-mar 152
                }
2 daniel-mar 153
        }
154
 
566 daniel-mar 155
        public function isRaLoggedIn($email) {
585 daniel-mar 156
                $acs = $this->getAuthContentStore();
157
                if (is_null($acs)) return false;
158
                return $acs->isRaLoggedIn($email);
2 daniel-mar 159
        }
160
 
585 daniel-mar 161
        // "High level" function including logging and checking for valid JWT alternations
617 daniel-mar 162
        public function raLoginEx($email, $remember_me, $origin='') {
585 daniel-mar 163
                $loginfo = '';
164
                $acs = $this->getAuthContentStore();
165
                if (!is_null($acs)) {
166
                        $acs->raLoginEx($email, $loginfo);
167
                        $acs->activate();
168
                } else {
169
                        if ($remember_me) {
170
                                if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_USER', true)) {
171
                                        throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_USER'));
172
                                }
173
                                $ttl = OIDplus::baseConfig()->getValue('JWT_TTL_LOGIN_USER', 10*365*24*60*60);
174
                                $authSimulation = new OIDplusAuthContentStoreJWT();
175
                                $authSimulation->raLoginEx($email, $loginfo);
176
                                $authSimulation->setValue('oidplus_generator', OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN);
177
                                $authSimulation->setValue('exp', time()+$ttl); // JWT "exp" attribute
178
                                $authSimulation->activate();
179
                        } else {
180
                                $authSimulation = new OIDplusAuthContentStoreSession();
181
                                $authSimulation->raLoginEx($email, $loginfo);
182
                                $authSimulation->activate();
183
                        }
184
                }
185
                $logmsg = "RA '$email' logged in";
186
                if ($origin != '') $logmsg .= " via $origin";
187
                if ($loginfo != '') $logmsg .= " ($loginfo)";
188
                OIDplus::logger()->log("[OK]RA($email)!", $logmsg);
189
        }
190
 
191
        public function raLogoutEx($email) {
192
                $loginfo = '';
193
 
194
                $acs = $this->getAuthContentStore();
195
                if (is_null($acs)) return;
196
                $res = $acs->raLogoutEx($email, $loginfo);
197
 
198
                OIDplus::logger()->log("[OK]RA($email)!", "RA '$email' logged out ($loginfo)");
199
 
200
                if (($this->raNumLoggedIn() == 0) && (!$this->isAdminLoggedIn())) {
201
                        // Nobody logged in anymore. Destroy session cookie to make GDPR people happy
202
                        $acs->destroySession();
203
                } else {
204
                        // Get a new token for the remaining users
205
                        $acs->activate();
206
                }
207
 
208
                return $res;
209
        }
210
 
14 daniel-mar 211
        // Admin authentication functions
2 daniel-mar 212
 
566 daniel-mar 213
        public function adminLogin() {
585 daniel-mar 214
                $acs = $this->getAuthContentStore();
215
                if (is_null($acs)) return;
216
                return $acs->adminLogin();
2 daniel-mar 217
        }
218
 
566 daniel-mar 219
        public function adminLogout() {
585 daniel-mar 220
                $acs = $this->getAuthContentStore();
221
                if (is_null($acs)) return;
222
                return $acs->adminLogout();
2 daniel-mar 223
        }
224
 
566 daniel-mar 225
        public function adminCheckPassword($password) {
609 daniel-mar 226
                $cfgData = OIDplus::baseConfig()->getValue('ADMIN_PASSWORD', '');
227
                if (empty($cfgData)) {
360 daniel-mar 228
                        throw new OIDplusException(_L('No admin password set in %1','userdata/baseconfig/config.inc.php'));
261 daniel-mar 229
                }
622 daniel-mar 230
 
609 daniel-mar 231
                if (!is_array($cfgData)) {
232
                        $passwordDataArray = array($cfgData);
233
                } else {
234
                        $passwordDataArray = $cfgData;
235
                }
622 daniel-mar 236
 
609 daniel-mar 237
                foreach ($passwordDataArray as $passwordData) {
238
                        if (strpos($passwordData, '$') !== false) {
239
                                if ($passwordData[0] == '$') {
240
                                        // Version 3: BCrypt
241
                                        return password_verify($password, $passwordData);
242
                                } else {
243
                                        // Version 2: SHA3-512 with salt
244
                                        list($s_salt, $hash) = explode('$', $passwordData, 2);
245
                                }
456 daniel-mar 246
                        } else {
609 daniel-mar 247
                                // Version 1: SHA3-512 without salt
248
                                $s_salt = '';
249
                                $hash = $passwordData;
456 daniel-mar 250
                        }
609 daniel-mar 251
 
617 daniel-mar 252
                        if (hash_equals(sha3_512($s_salt.$password, true), base64_decode($hash))) return true;
421 daniel-mar 253
                }
609 daniel-mar 254
 
255
                return false;
2 daniel-mar 256
        }
257
 
566 daniel-mar 258
        public function isAdminLoggedIn() {
585 daniel-mar 259
                if ($this->forceAllLoggedOut()) {
567 daniel-mar 260
                        return false;
261
                } else {
585 daniel-mar 262
                        $acs = $this->getAuthContentStore();
263
                        if (is_null($acs)) return false;
264
                        return $acs->isAdminLoggedIn();
567 daniel-mar 265
                }
2 daniel-mar 266
        }
267
 
585 daniel-mar 268
        // "High level" function including logging and checking for valid JWT alternations
617 daniel-mar 269
        public function adminLoginEx($remember_me, $origin='') {
585 daniel-mar 270
                $loginfo = '';
271
                $acs = $this->getAuthContentStore();
272
                if (!is_null($acs)) {
273
                        $acs->adminLoginEx($loginfo);
274
                        $acs->activate();
275
                } else {
276
                        if ($remember_me) {
277
                                if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) {
278
                                        throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_ADMIN'));
279
                                }
280
                                $ttl = OIDplus::baseConfig()->getValue('JWT_TTL_LOGIN_ADMIN', 10*365*24*60*60);
281
                                $authSimulation = new OIDplusAuthContentStoreJWT();
282
                                $authSimulation->adminLoginEx($loginfo);
283
                                $authSimulation->setValue('oidplus_generator', OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN);
284
                                $authSimulation->setValue('exp', time()+$ttl); // JWT "exp" attribute
285
                                $authSimulation->activate();
286
                        } else {
287
                                $authSimulation = new OIDplusAuthContentStoreSession();
288
                                $authSimulation->adminLoginEx($loginfo);
289
                                $authSimulation->activate();
290
                        }
291
                }
292
                $logmsg = "Admin logged in";
293
                if ($origin != '') $logmsg .= " via $origin";
294
                if ($loginfo != '') $logmsg .= " ($loginfo)";
295
                OIDplus::logger()->log("[OK]A!", $logmsg);
296
        }
297
 
298
        public function adminLogoutEx() {
299
                $loginfo = '';
300
 
301
                $acs = $this->getAuthContentStore();
302
                if (is_null($acs)) return;
303
                $res = $acs->adminLogoutEx($loginfo);
304
 
305
                if ($this->raNumLoggedIn() == 0) {
306
                        // Nobody here anymore. Destroy the cookie to make GDPR people happy
307
                        $acs->destroySession();
308
                } else {
309
                        // Get a new token for the remaining users
310
                        $acs->activate();
311
                }
312
 
313
                OIDplus::logger()->log("[OK]A!", "Admin logged out ($loginfo)");
314
                return $res;
315
        }
316
 
310 daniel-mar 317
        // Authentication keys for validating arguments (e.g. sent by mail)
2 daniel-mar 318
 
319
        public static function makeAuthKey($data) {
711 daniel-mar 320
                return sha3_512_hmac($data, 'authkey:'.OIDplus::baseConfig()->getValue('SERVER_SECRET'), false);
2 daniel-mar 321
        }
322
 
323
        public static function validateAuthKey($data, $auth_key) {
617 daniel-mar 324
                return hash_equals(self::makeAuthKey($data), $auth_key);
2 daniel-mar 325
        }
326
 
329 daniel-mar 327
        // "Veto" functions to force logout state
328
 
585 daniel-mar 329
        protected function forceAllLoggedOut() {
329 daniel-mar 330
                if (isset($_SERVER['SCRIPT_FILENAME']) && (basename($_SERVER['SCRIPT_FILENAME']) == 'sitemap.php')) {
331
                        // The sitemap may not contain any confidential information,
332
                        // even if the user is logged in, because the admin could
333
                        // accidentally copy-paste the sitemap to a
334
                        // search engine control panel while they are logged in
335
                        return true;
336
                } else {
337
                        return false;
338
                }
339
        }
427 daniel-mar 340
 
424 daniel-mar 341
        // CSRF functions
427 daniel-mar 342
 
424 daniel-mar 343
        private $enable_csrf = true;
427 daniel-mar 344
 
424 daniel-mar 345
        public function enableCSRF() {
346
                $this->enable_csrf = true;
347
        }
427 daniel-mar 348
 
424 daniel-mar 349
        public function disableCSRF() {
350
                $this->enable_csrf = false;
351
        }
427 daniel-mar 352
 
424 daniel-mar 353
        public function genCSRFToken() {
564 daniel-mar 354
                return bin2hex(self::getRandomBytes(64));
424 daniel-mar 355
        }
427 daniel-mar 356
 
424 daniel-mar 357
        public function checkCSRF() {
427 daniel-mar 358
                if (!$this->enable_csrf) return;
866 daniel-mar 359
 
360
                $request_token = isset($_REQUEST['csrf_token']) ? $_REQUEST['csrf_token'] : '';
361
                $cookie_token = isset($_COOKIE['csrf_token']) ? $_COOKIE['csrf_token'] : '';
362
 
363
                if (empty($request_token) || empty($cookie_token) || ($request_token !== $cookie_token)) {
364
                        if (OIDplus::baseConfig()->getValue('DEBUG')) {
365
                                throw new OIDplusException(_L('Missing or wrong CSRF Token: Request %1 vs Cookie %2',
366
                                        isset($_REQUEST['csrf_token']) ? '"'.$_REQUEST['csrf_token'].'"' : 'NULL',
367
                                        isset($_COOKIE['csrf_token']) ? $_COOKIE['csrf_token'] : 'NULL'
368
                                ));
369
                        } else {
370
                                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.'));
371
                        }
424 daniel-mar 372
                }
373
        }
329 daniel-mar 374
 
421 daniel-mar 375
        // Generate RA passwords
622 daniel-mar 376
 
461 daniel-mar 377
        public static function raGeneratePassword($password): OIDplusRAAuthInfo {
453 daniel-mar 378
                $def_method = OIDplus::config()->getValue('default_ra_auth_method');
379
 
380
                $plugins = OIDplus::getAuthPlugins();
381
                foreach ($plugins as $plugin) {
382
                        if (basename($plugin->getPluginDirectory()) === $def_method) {
617 daniel-mar 383
                                return $plugin->generate(self::raPepperProcessing($password));
453 daniel-mar 384
                        }
385
                }
386
                throw new OIDplusException(_L('Default RA auth method/plugin "%1" not found',$def_method));
421 daniel-mar 387
        }
388
 
389
        // Generate admin password
390
 
585 daniel-mar 391
        /* Nothing here; the admin password will be generated in setup_base.js , purely in the web-browser */
421 daniel-mar 392
 
393
}