Subversion Repositories oidplus

Rev

Rev 712 | Rev 849 | 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
511 daniel-mar 5
 * Copyright 2019 - 2021 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
 
511 daniel-mar 20
if (!defined('INSIDE_OIDPLUS')) die();
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')) {
466 daniel-mar 33
                        $a = bin2hex(mcrypt_create_iv($len, MCRYPT_DEV_URANDOM));
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;
564 daniel-mar 355
                if (!isset($_REQUEST['csrf_token']) || !isset($_COOKIE['csrf_token']) || ($_REQUEST['csrf_token'] !== $_COOKIE['csrf_token'])) {
356
                        throw new OIDplusException(_L('Wrong CSRF Token'));
424 daniel-mar 357
                }
358
        }
329 daniel-mar 359
 
421 daniel-mar 360
        // Generate RA passwords
622 daniel-mar 361
 
461 daniel-mar 362
        public static function raGeneratePassword($password): OIDplusRAAuthInfo {
453 daniel-mar 363
                $def_method = OIDplus::config()->getValue('default_ra_auth_method');
364
 
365
                $plugins = OIDplus::getAuthPlugins();
366
                foreach ($plugins as $plugin) {
367
                        if (basename($plugin->getPluginDirectory()) === $def_method) {
617 daniel-mar 368
                                return $plugin->generate(self::raPepperProcessing($password));
453 daniel-mar 369
                        }
370
                }
371
                throw new OIDplusException(_L('Default RA auth method/plugin "%1" not found',$def_method));
421 daniel-mar 372
        }
373
 
374
        // Generate admin password
375
 
585 daniel-mar 376
        /* Nothing here; the admin password will be generated in setup_base.js , purely in the web-browser */
421 daniel-mar 377
 
378
}