Subversion Repositories oidplus

Rev

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