Subversion Repositories oidplus

Rev

Rev 1096 | Rev 1099 | 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
1086 daniel-mar 5
 * Copyright 2019 - 2023 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
 
1050 daniel-mar 20
namespace ViaThinkSoft\OIDplus;
511 daniel-mar 21
 
1086 daniel-mar 22
// phpcs:disable PSR1.Files.SideEffects
23
\defined('INSIDE_OIDPLUS') or die;
24
// phpcs:enable PSR1.Files.SideEffects
25
 
730 daniel-mar 26
class OIDplusAuthUtils extends OIDplusBaseClass {
2 daniel-mar 27
 
566 daniel-mar 28
        // Useful functions
29
 
617 daniel-mar 30
        private static function raPepperProcessing(string $password): string {
31
                // Additional feature: Pepper
32
                // The pepper is stored inside the base configuration file
33
                // It prevents that an attacker with SQL write rights can
34
                // create accounts.
35
                // ATTENTION!!! If a pepper is used, then the
36
                // hashes are bound to that pepper. If you change the pepper,
37
                // then ALL passwords of RAs become INVALID!
38
                $pepper = OIDplus::baseConfig()->getValue('RA_PASSWORD_PEPPER','');
39
                if ($pepper !== '') {
711 daniel-mar 40
                        $algo = OIDplus::baseConfig()->getValue('RA_PASSWORD_PEPPER_ALGO','sha512'); // sha512 works with PHP 7.0
41
                        if (strtolower($algo) === 'sha3-512') {
42
                                $hmac = sha3_512_hmac($password, $pepper);
43
                        } else {
44
                                $hmac = hash_hmac($algo, $password, $pepper);
45
                        }
712 daniel-mar 46
                        if ($hmac === false) throw new OIDplusException(_L('HMAC failed'));
617 daniel-mar 47
                        return $hmac;
48
                } else {
49
                        return $password;
50
                }
51
        }
52
 
585 daniel-mar 53
        // Content provider
577 daniel-mar 54
 
585 daniel-mar 55
        public function getAuthMethod() {
56
                $acs = $this->getAuthContentStore();
57
                if (is_null($acs)) return 'null';
58
                return get_class($acs);
577 daniel-mar 59
        }
60
 
585 daniel-mar 61
        protected function getAuthContentStore() {
62
                // Logged in via JWT
63
                $tmp = OIDplusAuthContentStoreJWT::getActiveProvider();
64
                if ($tmp) return $tmp;
577 daniel-mar 65
 
585 daniel-mar 66
                // Normal login via web-browser
67
                // Cookie will only be created once content is stored
68
                $tmp = OIDplusAuthContentStoreSession::getActiveProvider();
69
                if ($tmp) return $tmp;
577 daniel-mar 70
 
585 daniel-mar 71
                // No active session and no JWT token available. User is not logged in.
72
                return null;
577 daniel-mar 73
        }
74
 
585 daniel-mar 75
        public function getExtendedAttribute($name, $default=NULL) {
76
                $acs = $this->getAuthContentStore();
77
                if (is_null($acs)) return $default;
78
                return $acs->getValue($name, $default);
577 daniel-mar 79
        }
80
 
566 daniel-mar 81
        // RA authentication functions
2 daniel-mar 82
 
566 daniel-mar 83
        public function raLogin($email) {
585 daniel-mar 84
                $acs = $this->getAuthContentStore();
85
                if (is_null($acs)) return;
86
                return $acs->raLogin($email);
566 daniel-mar 87
        }
2 daniel-mar 88
 
566 daniel-mar 89
        public function raLogout($email) {
585 daniel-mar 90
                $acs = $this->getAuthContentStore();
91
                if (is_null($acs)) return;
92
                return $acs->raLogout($email);
2 daniel-mar 93
        }
94
 
566 daniel-mar 95
        public function raCheckPassword($ra_email, $password) {
329 daniel-mar 96
                $ra = new OIDplusRA($ra_email);
453 daniel-mar 97
 
617 daniel-mar 98
                // Get RA info from RA
459 daniel-mar 99
                $authInfo = $ra->getAuthInfo();
590 daniel-mar 100
                if (!$authInfo) return false; // user not found
622 daniel-mar 101
 
617 daniel-mar 102
                // Ask plugins if they can verify this hash
453 daniel-mar 103
                $plugins = OIDplus::getAuthPlugins();
104
                if (count($plugins) == 0) {
105
                        throw new OIDplusException(_L('No RA authentication plugins found'));
106
                }
107
                foreach ($plugins as $plugin) {
617 daniel-mar 108
                        if ($plugin->verify($authInfo, self::raPepperProcessing($password))) return true;
453 daniel-mar 109
                }
110
 
111
                return false;
329 daniel-mar 112
        }
113
 
566 daniel-mar 114
        public function raNumLoggedIn() {
585 daniel-mar 115
                $acs = $this->getAuthContentStore();
116
                if (is_null($acs)) return 0;
117
                return $acs->raNumLoggedIn();
85 daniel-mar 118
        }
119
 
566 daniel-mar 120
        public function loggedInRaList() {
585 daniel-mar 121
                if ($this->forceAllLoggedOut()) {
567 daniel-mar 122
                        return array();
123
                } else {
585 daniel-mar 124
                        $acs = $this->getAuthContentStore();
125
                        if (is_null($acs)) return array();
126
                        return $acs->loggedInRaList();
567 daniel-mar 127
                }
2 daniel-mar 128
        }
129
 
566 daniel-mar 130
        public function isRaLoggedIn($email) {
585 daniel-mar 131
                $acs = $this->getAuthContentStore();
132
                if (is_null($acs)) return false;
133
                return $acs->isRaLoggedIn($email);
2 daniel-mar 134
        }
135
 
585 daniel-mar 136
        // "High level" function including logging and checking for valid JWT alternations
617 daniel-mar 137
        public function raLoginEx($email, $remember_me, $origin='') {
585 daniel-mar 138
                $loginfo = '';
139
                $acs = $this->getAuthContentStore();
140
                if (!is_null($acs)) {
141
                        $acs->raLoginEx($email, $loginfo);
142
                        $acs->activate();
143
                } else {
144
                        if ($remember_me) {
145
                                if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_USER', true)) {
146
                                        throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_USER'));
147
                                }
148
                                $ttl = OIDplus::baseConfig()->getValue('JWT_TTL_LOGIN_USER', 10*365*24*60*60);
149
                                $authSimulation = new OIDplusAuthContentStoreJWT();
150
                                $authSimulation->raLoginEx($email, $loginfo);
151
                                $authSimulation->setValue('oidplus_generator', OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN);
152
                                $authSimulation->setValue('exp', time()+$ttl); // JWT "exp" attribute
153
                                $authSimulation->activate();
154
                        } else {
155
                                $authSimulation = new OIDplusAuthContentStoreSession();
156
                                $authSimulation->raLoginEx($email, $loginfo);
157
                                $authSimulation->activate();
158
                        }
159
                }
160
                $logmsg = "RA '$email' logged in";
161
                if ($origin != '') $logmsg .= " via $origin";
162
                if ($loginfo != '') $logmsg .= " ($loginfo)";
163
                OIDplus::logger()->log("[OK]RA($email)!", $logmsg);
164
        }
165
 
166
        public function raLogoutEx($email) {
167
                $loginfo = '';
168
 
169
                $acs = $this->getAuthContentStore();
170
                if (is_null($acs)) return;
171
                $res = $acs->raLogoutEx($email, $loginfo);
172
 
173
                OIDplus::logger()->log("[OK]RA($email)!", "RA '$email' logged out ($loginfo)");
174
 
175
                if (($this->raNumLoggedIn() == 0) && (!$this->isAdminLoggedIn())) {
176
                        // Nobody logged in anymore. Destroy session cookie to make GDPR people happy
177
                        $acs->destroySession();
178
                } else {
179
                        // Get a new token for the remaining users
180
                        $acs->activate();
181
                }
182
 
183
                return $res;
184
        }
185
 
14 daniel-mar 186
        // Admin authentication functions
2 daniel-mar 187
 
566 daniel-mar 188
        public function adminLogin() {
585 daniel-mar 189
                $acs = $this->getAuthContentStore();
190
                if (is_null($acs)) return;
191
                return $acs->adminLogin();
2 daniel-mar 192
        }
193
 
566 daniel-mar 194
        public function adminLogout() {
585 daniel-mar 195
                $acs = $this->getAuthContentStore();
196
                if (is_null($acs)) return;
197
                return $acs->adminLogout();
2 daniel-mar 198
        }
199
 
566 daniel-mar 200
        public function adminCheckPassword($password) {
609 daniel-mar 201
                $cfgData = OIDplus::baseConfig()->getValue('ADMIN_PASSWORD', '');
202
                if (empty($cfgData)) {
360 daniel-mar 203
                        throw new OIDplusException(_L('No admin password set in %1','userdata/baseconfig/config.inc.php'));
261 daniel-mar 204
                }
622 daniel-mar 205
 
609 daniel-mar 206
                if (!is_array($cfgData)) {
207
                        $passwordDataArray = array($cfgData);
208
                } else {
209
                        $passwordDataArray = $cfgData;
210
                }
622 daniel-mar 211
 
609 daniel-mar 212
                foreach ($passwordDataArray as $passwordData) {
213
                        if (strpos($passwordData, '$') !== false) {
214
                                if ($passwordData[0] == '$') {
215
                                        // Version 3: BCrypt
216
                                        return password_verify($password, $passwordData);
217
                                } else {
218
                                        // Version 2: SHA3-512 with salt
219
                                        list($s_salt, $hash) = explode('$', $passwordData, 2);
220
                                }
456 daniel-mar 221
                        } else {
609 daniel-mar 222
                                // Version 1: SHA3-512 without salt
223
                                $s_salt = '';
224
                                $hash = $passwordData;
456 daniel-mar 225
                        }
609 daniel-mar 226
 
617 daniel-mar 227
                        if (hash_equals(sha3_512($s_salt.$password, true), base64_decode($hash))) return true;
421 daniel-mar 228
                }
609 daniel-mar 229
 
230
                return false;
2 daniel-mar 231
        }
232
 
566 daniel-mar 233
        public function isAdminLoggedIn() {
585 daniel-mar 234
                if ($this->forceAllLoggedOut()) {
567 daniel-mar 235
                        return false;
236
                } else {
585 daniel-mar 237
                        $acs = $this->getAuthContentStore();
238
                        if (is_null($acs)) return false;
239
                        return $acs->isAdminLoggedIn();
567 daniel-mar 240
                }
2 daniel-mar 241
        }
242
 
585 daniel-mar 243
        // "High level" function including logging and checking for valid JWT alternations
617 daniel-mar 244
        public function adminLoginEx($remember_me, $origin='') {
585 daniel-mar 245
                $loginfo = '';
246
                $acs = $this->getAuthContentStore();
247
                if (!is_null($acs)) {
248
                        $acs->adminLoginEx($loginfo);
249
                        $acs->activate();
250
                } else {
251
                        if ($remember_me) {
252
                                if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) {
253
                                        throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_ADMIN'));
254
                                }
255
                                $ttl = OIDplus::baseConfig()->getValue('JWT_TTL_LOGIN_ADMIN', 10*365*24*60*60);
256
                                $authSimulation = new OIDplusAuthContentStoreJWT();
257
                                $authSimulation->adminLoginEx($loginfo);
258
                                $authSimulation->setValue('oidplus_generator', OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN);
259
                                $authSimulation->setValue('exp', time()+$ttl); // JWT "exp" attribute
260
                                $authSimulation->activate();
261
                        } else {
262
                                $authSimulation = new OIDplusAuthContentStoreSession();
263
                                $authSimulation->adminLoginEx($loginfo);
264
                                $authSimulation->activate();
265
                        }
266
                }
267
                $logmsg = "Admin logged in";
268
                if ($origin != '') $logmsg .= " via $origin";
269
                if ($loginfo != '') $logmsg .= " ($loginfo)";
270
                OIDplus::logger()->log("[OK]A!", $logmsg);
271
        }
272
 
273
        public function adminLogoutEx() {
274
                $loginfo = '';
275
 
276
                $acs = $this->getAuthContentStore();
277
                if (is_null($acs)) return;
278
                $res = $acs->adminLogoutEx($loginfo);
279
 
280
                if ($this->raNumLoggedIn() == 0) {
281
                        // Nobody here anymore. Destroy the cookie to make GDPR people happy
282
                        $acs->destroySession();
283
                } else {
284
                        // Get a new token for the remaining users
285
                        $acs->activate();
286
                }
287
 
288
                OIDplus::logger()->log("[OK]A!", "Admin logged out ($loginfo)");
289
                return $res;
290
        }
291
 
310 daniel-mar 292
        // Authentication keys for validating arguments (e.g. sent by mail)
2 daniel-mar 293
 
294
        public static function makeAuthKey($data) {
711 daniel-mar 295
                return sha3_512_hmac($data, 'authkey:'.OIDplus::baseConfig()->getValue('SERVER_SECRET'), false);
2 daniel-mar 296
        }
297
 
298
        public static function validateAuthKey($data, $auth_key) {
617 daniel-mar 299
                return hash_equals(self::makeAuthKey($data), $auth_key);
2 daniel-mar 300
        }
301
 
329 daniel-mar 302
        // "Veto" functions to force logout state
303
 
585 daniel-mar 304
        protected function forceAllLoggedOut() {
329 daniel-mar 305
                if (isset($_SERVER['SCRIPT_FILENAME']) && (basename($_SERVER['SCRIPT_FILENAME']) == 'sitemap.php')) {
306
                        // The sitemap may not contain any confidential information,
307
                        // even if the user is logged in, because the admin could
308
                        // accidentally copy-paste the sitemap to a
309
                        // search engine control panel while they are logged in
310
                        return true;
311
                } else {
312
                        return false;
313
                }
314
        }
427 daniel-mar 315
 
424 daniel-mar 316
        // CSRF functions
427 daniel-mar 317
 
424 daniel-mar 318
        private $enable_csrf = true;
427 daniel-mar 319
 
424 daniel-mar 320
        public function enableCSRF() {
321
                $this->enable_csrf = true;
322
        }
427 daniel-mar 323
 
424 daniel-mar 324
        public function disableCSRF() {
325
                $this->enable_csrf = false;
326
        }
427 daniel-mar 327
 
424 daniel-mar 328
        public function genCSRFToken() {
1098 daniel-mar 329
                return random_bytes_ex(64, false, false);
424 daniel-mar 330
        }
427 daniel-mar 331
 
424 daniel-mar 332
        public function checkCSRF() {
427 daniel-mar 333
                if (!$this->enable_csrf) return;
866 daniel-mar 334
 
335
                $request_token = isset($_REQUEST['csrf_token']) ? $_REQUEST['csrf_token'] : '';
336
                $cookie_token = isset($_COOKIE['csrf_token']) ? $_COOKIE['csrf_token'] : '';
337
 
338
                if (empty($request_token) || empty($cookie_token) || ($request_token !== $cookie_token)) {
339
                        if (OIDplus::baseConfig()->getValue('DEBUG')) {
340
                                throw new OIDplusException(_L('Missing or wrong CSRF Token: Request %1 vs Cookie %2',
341
                                        isset($_REQUEST['csrf_token']) ? '"'.$_REQUEST['csrf_token'].'"' : 'NULL',
342
                                        isset($_COOKIE['csrf_token']) ? $_COOKIE['csrf_token'] : 'NULL'
343
                                ));
344
                        } else {
345
                                throw new OIDplusException(_L('Missing or wrong "CSRF Token". To fix the issue, try clearing your browser cache and reload the page. If you visited the page via HTTPS before, try HTTPS in case you are currently connected via HTTP.'));
346
                        }
424 daniel-mar 347
                }
348
        }
329 daniel-mar 349
 
421 daniel-mar 350
        // Generate RA passwords
622 daniel-mar 351
 
461 daniel-mar 352
        public static function raGeneratePassword($password): OIDplusRAAuthInfo {
1088 daniel-mar 353
                $plugin = OIDplus::getDefaultRaAuthPlugin();
354
                return $plugin->generate(self::raPepperProcessing($password));
421 daniel-mar 355
        }
356
 
357
        // Generate admin password
358
 
585 daniel-mar 359
        /* Nothing here; the admin password will be generated in setup_base.js , purely in the web-browser */
421 daniel-mar 360
 
361
}
1096 daniel-mar 362