Subversion Repositories oidplus

Rev

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