Subversion Repositories oidplus

Rev

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