Subversion Repositories oidplus

Rev

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