Subversion Repositories oidplus

Rev

Rev 585 | Rev 592 | 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();
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) {
421 daniel-mar 197
                $passwordData = OIDplus::baseConfig()->getValue('ADMIN_PASSWORD', '');
198
                if (empty($passwordData)) {
360 daniel-mar 199
                        throw new OIDplusException(_L('No admin password set in %1','userdata/baseconfig/config.inc.php'));
261 daniel-mar 200
                }
456 daniel-mar 201
 
421 daniel-mar 202
                if (strpos($passwordData, '$') !== false) {
456 daniel-mar 203
                        if ($passwordData[0] == '$') {
204
                                // Version 3: BCrypt
205
                                return password_verify($password, $passwordData);
206
                        } else {
457 daniel-mar 207
                                // Version 2: SHA3-512 with salt
456 daniel-mar 208
                                list($s_salt, $hash) = explode('$', $passwordData, 2);
209
                        }
421 daniel-mar 210
                } else {
456 daniel-mar 211
                        // Version 1: SHA3-512 without salt
421 daniel-mar 212
                        $s_salt = '';
213
                        $hash = $passwordData;
214
                }
215
                return strcmp(sha3_512($s_salt.$password, true), base64_decode($hash)) === 0;
2 daniel-mar 216
        }
217
 
566 daniel-mar 218
        public function isAdminLoggedIn() {
585 daniel-mar 219
                if ($this->forceAllLoggedOut()) {
567 daniel-mar 220
                        return false;
221
                } else {
585 daniel-mar 222
                        $acs = $this->getAuthContentStore();
223
                        if (is_null($acs)) return false;
224
                        return $acs->isAdminLoggedIn();
567 daniel-mar 225
                }
2 daniel-mar 226
        }
227
 
585 daniel-mar 228
        // "High level" function including logging and checking for valid JWT alternations
229
        public function adminLoginEx($remember_me, $origin) {
230
                $loginfo = '';
231
                $acs = $this->getAuthContentStore();
232
                if (!is_null($acs)) {
233
                        $acs->adminLoginEx($loginfo);
234
                        $acs->activate();
235
                } else {
236
                        if ($remember_me) {
237
                                if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) {
238
                                        throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_ADMIN'));
239
                                }
240
                                $ttl = OIDplus::baseConfig()->getValue('JWT_TTL_LOGIN_ADMIN', 10*365*24*60*60);
241
                                $authSimulation = new OIDplusAuthContentStoreJWT();
242
                                $authSimulation->adminLoginEx($loginfo);
243
                                $authSimulation->setValue('oidplus_generator', OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN);
244
                                $authSimulation->setValue('exp', time()+$ttl); // JWT "exp" attribute
245
                                $authSimulation->activate();
246
                        } else {
247
                                $authSimulation = new OIDplusAuthContentStoreSession();
248
                                $authSimulation->adminLoginEx($loginfo);
249
                                $authSimulation->activate();
250
                        }
251
                }
252
                $logmsg = "Admin logged in";
253
                if ($origin != '') $logmsg .= " via $origin";
254
                if ($loginfo != '') $logmsg .= " ($loginfo)";
255
                OIDplus::logger()->log("[OK]A!", $logmsg);
256
        }
257
 
258
        public function adminLogoutEx() {
259
                $loginfo = '';
260
 
261
                $acs = $this->getAuthContentStore();
262
                if (is_null($acs)) return;
263
                $res = $acs->adminLogoutEx($loginfo);
264
 
265
                if ($this->raNumLoggedIn() == 0) {
266
                        // Nobody here anymore. Destroy the cookie to make GDPR people happy
267
                        $acs->destroySession();
268
                } else {
269
                        // Get a new token for the remaining users
270
                        $acs->activate();
271
                }
272
 
273
                OIDplus::logger()->log("[OK]A!", "Admin logged out ($loginfo)");
274
                return $res;
275
        }
276
 
310 daniel-mar 277
        // Authentication keys for validating arguments (e.g. sent by mail)
2 daniel-mar 278
 
279
        public static function makeAuthKey($data) {
424 daniel-mar 280
                $data = OIDplus::baseConfig()->getValue('SERVER_SECRET') . '/AUTHKEY/' . $data;
421 daniel-mar 281
                $calc_authkey = sha3_512($data, false);
2 daniel-mar 282
                return $calc_authkey;
283
        }
284
 
285
        public static function validateAuthKey($data, $auth_key) {
421 daniel-mar 286
                return strcmp(self::makeAuthKey($data), $auth_key) === 0;
2 daniel-mar 287
        }
288
 
329 daniel-mar 289
        // "Veto" functions to force logout state
290
 
585 daniel-mar 291
        protected function forceAllLoggedOut() {
329 daniel-mar 292
                if (isset($_SERVER['SCRIPT_FILENAME']) && (basename($_SERVER['SCRIPT_FILENAME']) == 'sitemap.php')) {
293
                        // The sitemap may not contain any confidential information,
294
                        // even if the user is logged in, because the admin could
295
                        // accidentally copy-paste the sitemap to a
296
                        // search engine control panel while they are logged in
297
                        return true;
298
                } else {
299
                        return false;
300
                }
301
        }
427 daniel-mar 302
 
424 daniel-mar 303
        // CSRF functions
427 daniel-mar 304
 
424 daniel-mar 305
        private $enable_csrf = true;
427 daniel-mar 306
 
424 daniel-mar 307
        public function enableCSRF() {
308
                $this->enable_csrf = true;
309
        }
427 daniel-mar 310
 
424 daniel-mar 311
        public function disableCSRF() {
312
                $this->enable_csrf = false;
313
        }
427 daniel-mar 314
 
424 daniel-mar 315
        public function genCSRFToken() {
564 daniel-mar 316
                return bin2hex(self::getRandomBytes(64));
424 daniel-mar 317
        }
427 daniel-mar 318
 
424 daniel-mar 319
        public function checkCSRF() {
427 daniel-mar 320
                if (!$this->enable_csrf) return;
564 daniel-mar 321
                if (!isset($_REQUEST['csrf_token']) || !isset($_COOKIE['csrf_token']) || ($_REQUEST['csrf_token'] !== $_COOKIE['csrf_token'])) {
322
                        throw new OIDplusException(_L('Wrong CSRF Token'));
424 daniel-mar 323
                }
324
        }
329 daniel-mar 325
 
421 daniel-mar 326
        // Generate RA passwords
327
 
461 daniel-mar 328
        public static function raGeneratePassword($password): OIDplusRAAuthInfo {
453 daniel-mar 329
                $def_method = OIDplus::config()->getValue('default_ra_auth_method');
330
 
331
                $plugins = OIDplus::getAuthPlugins();
332
                foreach ($plugins as $plugin) {
333
                        if (basename($plugin->getPluginDirectory()) === $def_method) {
334
                                return $plugin->generate($password);
335
                        }
336
                }
337
                throw new OIDplusException(_L('Default RA auth method/plugin "%1" not found',$def_method));
421 daniel-mar 338
        }
339
 
340
        // Generate admin password
341
 
585 daniel-mar 342
        /* Nothing here; the admin password will be generated in setup_base.js , purely in the web-browser */
421 daniel-mar 343
 
344
}