Subversion Repositories oidplus

Rev

Rev 1199 | Rev 1267 | 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
 
1116 daniel-mar 30
        /**
31
         * @param string $password
32
         * @return string
33
         * @throws OIDplusException
34
         */
1186 daniel-mar 35
        private function raPepperProcessing(string $password): string {
617 daniel-mar 36
                // Additional feature: Pepper
37
                // The pepper is stored inside the base configuration file
38
                // It prevents that an attacker with SQL write rights can
39
                // create accounts.
40
                // ATTENTION!!! If a pepper is used, then the
41
                // hashes are bound to that pepper. If you change the pepper,
42
                // then ALL passwords of RAs become INVALID!
43
                $pepper = OIDplus::baseConfig()->getValue('RA_PASSWORD_PEPPER','');
44
                if ($pepper !== '') {
711 daniel-mar 45
                        $algo = OIDplus::baseConfig()->getValue('RA_PASSWORD_PEPPER_ALGO','sha512'); // sha512 works with PHP 7.0
46
                        if (strtolower($algo) === 'sha3-512') {
47
                                $hmac = sha3_512_hmac($password, $pepper);
48
                        } else {
49
                                $hmac = hash_hmac($algo, $password, $pepper);
50
                        }
1116 daniel-mar 51
                        if ($hmac === "") throw new OIDplusException(_L('HMAC failed'));
617 daniel-mar 52
                        return $hmac;
53
                } else {
54
                        return $password;
55
                }
56
        }
57
 
585 daniel-mar 58
        // Content provider
577 daniel-mar 59
 
1116 daniel-mar 60
        /**
61
         * @return string
62
         * @throws OIDplusException
63
         */
1130 daniel-mar 64
        public function getAuthMethod(): string {
585 daniel-mar 65
                $acs = $this->getAuthContentStore();
66
                if (is_null($acs)) return 'null';
67
                return get_class($acs);
577 daniel-mar 68
        }
69
 
1116 daniel-mar 70
        /**
71
         * @return OIDplusAuthContentStore|null
72
         * @throws OIDplusException
73
         */
74
        protected function getAuthContentStore()/*: ?OIDplusAuthContentStore*/ {
1265 daniel-mar 75
                // TODO: Should we implement these AuthContentStore as plugin type, so that there can be more than just JWT and PHP session?
76
 
585 daniel-mar 77
                // Logged in via JWT
78
                $tmp = OIDplusAuthContentStoreJWT::getActiveProvider();
79
                if ($tmp) return $tmp;
577 daniel-mar 80
 
1265 daniel-mar 81
                // For REST, we must only allow JWT from Bearer and nothing else! So disable cookies if we are accessing the REST plugin
82
                $rel_url = substr($_SERVER['REQUEST_URI'], strlen(OIDplus::webpath(null, OIDplus::PATH_RELATIVE_TO_ROOT)));
83
                if (!str_starts_with($rel_url, 'rest/')) { // <== TODO: Find a way how to move this into the plugin, since REST does not belong to the core. (Maybe some kind of "stateless mode" that is enabled by the REST plugin)
577 daniel-mar 84
 
1265 daniel-mar 85
                        // Normal login via web-browser
86
                        // Cookie will only be created once content is stored
87
                        $tmp = OIDplusAuthContentStoreSession::getActiveProvider();
88
                        if ($tmp) return $tmp;
89
 
90
                }
91
 
585 daniel-mar 92
                // No active session and no JWT token available. User is not logged in.
93
                return null;
577 daniel-mar 94
        }
95
 
1116 daniel-mar 96
        /**
97
         * @param string $name
98
         * @param mixed|null $default
99
         * @return mixed
100
         * @throws OIDplusException
101
         */
102
        public function getExtendedAttribute(string $name, $default=NULL) {
585 daniel-mar 103
                $acs = $this->getAuthContentStore();
104
                if (is_null($acs)) return $default;
105
                return $acs->getValue($name, $default);
577 daniel-mar 106
        }
107
 
566 daniel-mar 108
        // RA authentication functions
2 daniel-mar 109
 
1116 daniel-mar 110
        /**
111
         * @param string $email
112
         * @return void
113
         * @throws OIDplusException
114
         */
115
        public function raLogin(string $email) {
585 daniel-mar 116
                $acs = $this->getAuthContentStore();
117
                if (is_null($acs)) return;
1116 daniel-mar 118
                $acs->raLogin($email);
566 daniel-mar 119
        }
2 daniel-mar 120
 
1116 daniel-mar 121
        /**
122
         * @param string $email
123
         * @return void
124
         * @throws OIDplusException
125
         */
126
        public function raLogout(string $email) {
585 daniel-mar 127
                $acs = $this->getAuthContentStore();
128
                if (is_null($acs)) return;
1116 daniel-mar 129
                $acs->raLogout($email);
2 daniel-mar 130
        }
131
 
1116 daniel-mar 132
        /**
133
         * @param string $ra_email
134
         * @param string $password
135
         * @return bool
136
         * @throws OIDplusException
137
         */
138
        public function raCheckPassword(string $ra_email, string $password): bool {
329 daniel-mar 139
                $ra = new OIDplusRA($ra_email);
453 daniel-mar 140
 
617 daniel-mar 141
                // Get RA info from RA
459 daniel-mar 142
                $authInfo = $ra->getAuthInfo();
590 daniel-mar 143
                if (!$authInfo) return false; // user not found
622 daniel-mar 144
 
617 daniel-mar 145
                // Ask plugins if they can verify this hash
453 daniel-mar 146
                $plugins = OIDplus::getAuthPlugins();
147
                if (count($plugins) == 0) {
148
                        throw new OIDplusException(_L('No RA authentication plugins found'));
149
                }
150
                foreach ($plugins as $plugin) {
1186 daniel-mar 151
                        if ($plugin->verify($authInfo, $this->raPepperProcessing($password))) return true;
453 daniel-mar 152
                }
153
 
154
                return false;
329 daniel-mar 155
        }
156
 
1116 daniel-mar 157
        /**
158
         * @return int
159
         * @throws OIDplusException
160
         */
161
        public function raNumLoggedIn(): int {
585 daniel-mar 162
                $acs = $this->getAuthContentStore();
163
                if (is_null($acs)) return 0;
164
                return $acs->raNumLoggedIn();
85 daniel-mar 165
        }
166
 
1116 daniel-mar 167
        /**
168
         * @return OIDplusRA[]
169
         * @throws OIDplusException
170
         */
171
        public function loggedInRaList(): array {
585 daniel-mar 172
                if ($this->forceAllLoggedOut()) {
567 daniel-mar 173
                        return array();
174
                } else {
585 daniel-mar 175
                        $acs = $this->getAuthContentStore();
176
                        if (is_null($acs)) return array();
177
                        return $acs->loggedInRaList();
567 daniel-mar 178
                }
2 daniel-mar 179
        }
180
 
1116 daniel-mar 181
        /**
182
         * @param string $email
183
         * @return bool
184
         * @throws OIDplusException
185
         */
186
        public function isRaLoggedIn(string $email): bool {
585 daniel-mar 187
                $acs = $this->getAuthContentStore();
188
                if (is_null($acs)) return false;
189
                return $acs->isRaLoggedIn($email);
2 daniel-mar 190
        }
191
 
585 daniel-mar 192
        // "High level" function including logging and checking for valid JWT alternations
1116 daniel-mar 193
 
194
        /**
195
         * @param string $email
196
         * @param bool $remember_me
197
         * @param string $origin
198
         * @return void
199
         * @throws OIDplusException
200
         */
201
        public function raLoginEx(string $email, bool $remember_me, string $origin='') {
585 daniel-mar 202
                $loginfo = '';
203
                $acs = $this->getAuthContentStore();
204
                if (!is_null($acs)) {
205
                        $acs->raLoginEx($email, $loginfo);
206
                        $acs->activate();
207
                } else {
208
                        if ($remember_me) {
209
                                if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_USER', true)) {
210
                                        throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_USER'));
211
                                }
212
                                $ttl = OIDplus::baseConfig()->getValue('JWT_TTL_LOGIN_USER', 10*365*24*60*60);
213
                                $authSimulation = new OIDplusAuthContentStoreJWT();
214
                                $authSimulation->raLoginEx($email, $loginfo);
215
                                $authSimulation->setValue('oidplus_generator', OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN);
216
                                $authSimulation->setValue('exp', time()+$ttl); // JWT "exp" attribute
217
                                $authSimulation->activate();
218
                        } else {
219
                                $authSimulation = new OIDplusAuthContentStoreSession();
220
                                $authSimulation->raLoginEx($email, $loginfo);
221
                                $authSimulation->activate();
222
                        }
223
                }
224
                $logmsg = "RA '$email' logged in";
225
                if ($origin != '') $logmsg .= " via $origin";
226
                if ($loginfo != '') $logmsg .= " ($loginfo)";
1199 daniel-mar 227
                OIDplus::logger()->log("[OK]RA(%1)!", "%2", $email, $logmsg);
585 daniel-mar 228
        }
229
 
1116 daniel-mar 230
        /**
231
         * @param string $email
232
         * @return void
233
         * @throws OIDplusException
234
         */
235
        public function raLogoutEx(string $email) {
585 daniel-mar 236
                $loginfo = '';
237
 
238
                $acs = $this->getAuthContentStore();
239
                if (is_null($acs)) return;
1116 daniel-mar 240
                $acs->raLogoutEx($email, $loginfo);
585 daniel-mar 241
 
1199 daniel-mar 242
                OIDplus::logger()->log("[OK]RA(%1)!", "RA '%1' logged out (%2)", $email, $loginfo);
585 daniel-mar 243
 
244
                if (($this->raNumLoggedIn() == 0) && (!$this->isAdminLoggedIn())) {
245
                        // Nobody logged in anymore. Destroy session cookie to make GDPR people happy
246
                        $acs->destroySession();
247
                } else {
248
                        // Get a new token for the remaining users
249
                        $acs->activate();
250
                }
251
        }
252
 
14 daniel-mar 253
        // Admin authentication functions
2 daniel-mar 254
 
1116 daniel-mar 255
        /**
256
         * @return void
257
         * @throws OIDplusException
258
         */
566 daniel-mar 259
        public function adminLogin() {
585 daniel-mar 260
                $acs = $this->getAuthContentStore();
261
                if (is_null($acs)) return;
1116 daniel-mar 262
                $acs->adminLogin();
2 daniel-mar 263
        }
264
 
1116 daniel-mar 265
        /**
266
         * @return void
267
         * @throws OIDplusException
268
         */
566 daniel-mar 269
        public function adminLogout() {
585 daniel-mar 270
                $acs = $this->getAuthContentStore();
271
                if (is_null($acs)) return;
1116 daniel-mar 272
                $acs->adminLogout();
2 daniel-mar 273
        }
274
 
1116 daniel-mar 275
        /**
276
         * @param string $password
277
         * @return bool
278
         * @throws OIDplusException
279
         */
280
        public function adminCheckPassword(string $password): bool {
609 daniel-mar 281
                $cfgData = OIDplus::baseConfig()->getValue('ADMIN_PASSWORD', '');
282
                if (empty($cfgData)) {
360 daniel-mar 283
                        throw new OIDplusException(_L('No admin password set in %1','userdata/baseconfig/config.inc.php'));
261 daniel-mar 284
                }
622 daniel-mar 285
 
609 daniel-mar 286
                if (!is_array($cfgData)) {
287
                        $passwordDataArray = array($cfgData);
288
                } else {
1106 daniel-mar 289
                        $passwordDataArray = $cfgData; // Multiple Administrator passwords
609 daniel-mar 290
                }
622 daniel-mar 291
 
609 daniel-mar 292
                foreach ($passwordDataArray as $passwordData) {
1107 daniel-mar 293
                        if (str_starts_with($passwordData, '$')) {
294
                                // Version 3: BCrypt (or any other crypt)
295
                                $ok = password_verify($password, $passwordData);
296
                        } else if (strpos($passwordData, '$') !== false) {
297
                                // Version 2: SHA3-512 with salt
298
                                list($salt, $hash) = explode('$', $passwordData, 2);
299
                                $ok = hash_equals(sha3_512($salt.$password, true), base64_decode($hash));
456 daniel-mar 300
                        } else {
609 daniel-mar 301
                                // Version 1: SHA3-512 without salt
1107 daniel-mar 302
                                $ok = hash_equals(sha3_512($password, true), base64_decode($passwordData));
456 daniel-mar 303
                        }
1107 daniel-mar 304
                        if ($ok) return true;
421 daniel-mar 305
                }
609 daniel-mar 306
 
307
                return false;
2 daniel-mar 308
        }
309
 
1116 daniel-mar 310
        /**
311
         * @return bool
312
         * @throws OIDplusException
313
         */
314
        public function isAdminLoggedIn(): bool {
585 daniel-mar 315
                if ($this->forceAllLoggedOut()) {
567 daniel-mar 316
                        return false;
317
                } else {
585 daniel-mar 318
                        $acs = $this->getAuthContentStore();
319
                        if (is_null($acs)) return false;
320
                        return $acs->isAdminLoggedIn();
567 daniel-mar 321
                }
2 daniel-mar 322
        }
323
 
1116 daniel-mar 324
        /**
325
         * "High level" function including logging and checking for valid JWT alternations
326
         * @param bool $remember_me
327
         * @param string $origin
328
         * @return void
329
         * @throws OIDplusException
330
         */
331
        public function adminLoginEx(bool $remember_me, string $origin='') {
585 daniel-mar 332
                $loginfo = '';
333
                $acs = $this->getAuthContentStore();
334
                if (!is_null($acs)) {
335
                        $acs->adminLoginEx($loginfo);
336
                        $acs->activate();
337
                } else {
338
                        if ($remember_me) {
339
                                if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) {
340
                                        throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_ADMIN'));
341
                                }
342
                                $ttl = OIDplus::baseConfig()->getValue('JWT_TTL_LOGIN_ADMIN', 10*365*24*60*60);
343
                                $authSimulation = new OIDplusAuthContentStoreJWT();
344
                                $authSimulation->adminLoginEx($loginfo);
345
                                $authSimulation->setValue('oidplus_generator', OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN);
346
                                $authSimulation->setValue('exp', time()+$ttl); // JWT "exp" attribute
347
                                $authSimulation->activate();
348
                        } else {
349
                                $authSimulation = new OIDplusAuthContentStoreSession();
350
                                $authSimulation->adminLoginEx($loginfo);
351
                                $authSimulation->activate();
352
                        }
353
                }
354
                $logmsg = "Admin logged in";
355
                if ($origin != '') $logmsg .= " via $origin";
356
                if ($loginfo != '') $logmsg .= " ($loginfo)";
1199 daniel-mar 357
                OIDplus::logger()->log("[OK]A!", "%1", $logmsg);
585 daniel-mar 358
        }
359
 
1116 daniel-mar 360
        /**
361
         * @return void
362
         * @throws OIDplusException
363
         */
585 daniel-mar 364
        public function adminLogoutEx() {
365
                $loginfo = '';
366
 
367
                $acs = $this->getAuthContentStore();
368
                if (is_null($acs)) return;
1116 daniel-mar 369
                $acs->adminLogoutEx($loginfo);
585 daniel-mar 370
 
371
                if ($this->raNumLoggedIn() == 0) {
372
                        // Nobody here anymore. Destroy the cookie to make GDPR people happy
373
                        $acs->destroySession();
374
                } else {
375
                        // Get a new token for the remaining users
376
                        $acs->activate();
377
                }
378
 
1199 daniel-mar 379
                OIDplus::logger()->log("[OK]A!", "Admin logged out (%1)", $loginfo);
585 daniel-mar 380
        }
381
 
310 daniel-mar 382
        // Authentication keys for validating arguments (e.g. sent by mail)
2 daniel-mar 383
 
1116 daniel-mar 384
        /**
385
         * @param string $data
386
         * @return string
387
         * @throws OIDplusException
388
         */
1186 daniel-mar 389
        public function makeAuthKey(string $data): string {
711 daniel-mar 390
                return sha3_512_hmac($data, 'authkey:'.OIDplus::baseConfig()->getValue('SERVER_SECRET'), false);
2 daniel-mar 391
        }
392
 
1116 daniel-mar 393
        /**
394
         * @param string $data
395
         * @param string $auth_key
396
         * @return bool
397
         * @throws OIDplusException
398
         */
1186 daniel-mar 399
        public function validateAuthKey(string $data, string $auth_key): bool {
400
                return hash_equals($this->makeAuthKey($data), $auth_key);
2 daniel-mar 401
        }
402
 
329 daniel-mar 403
        // "Veto" functions to force logout state
404
 
1116 daniel-mar 405
        /**
406
         * @return bool
407
         */
408
        protected function forceAllLoggedOut(): bool {
329 daniel-mar 409
                if (isset($_SERVER['SCRIPT_FILENAME']) && (basename($_SERVER['SCRIPT_FILENAME']) == 'sitemap.php')) {
410
                        // The sitemap may not contain any confidential information,
411
                        // even if the user is logged in, because the admin could
412
                        // accidentally copy-paste the sitemap to a
413
                        // search engine control panel while they are logged in
414
                        return true;
415
                } else {
416
                        return false;
417
                }
418
        }
427 daniel-mar 419
 
424 daniel-mar 420
        // CSRF functions
427 daniel-mar 421
 
424 daniel-mar 422
        private $enable_csrf = true;
427 daniel-mar 423
 
1116 daniel-mar 424
        /**
425
         * @return void
426
         */
424 daniel-mar 427
        public function enableCSRF() {
428
                $this->enable_csrf = true;
429
        }
427 daniel-mar 430
 
1116 daniel-mar 431
        /**
432
         * @return void
433
         */
424 daniel-mar 434
        public function disableCSRF() {
435
                $this->enable_csrf = false;
436
        }
427 daniel-mar 437
 
1116 daniel-mar 438
        /**
439
         * @return string
440
         * @throws \Random\RandomException
441
         */
442
        public function genCSRFToken(): string {
1098 daniel-mar 443
                return random_bytes_ex(64, false, false);
424 daniel-mar 444
        }
427 daniel-mar 445
 
1116 daniel-mar 446
        /**
447
         * @return void
448
         * @throws OIDplusException
449
         */
424 daniel-mar 450
        public function checkCSRF() {
427 daniel-mar 451
                if (!$this->enable_csrf) return;
866 daniel-mar 452
 
1130 daniel-mar 453
                $request_token = $_REQUEST['csrf_token'] ?? '';
454
                $cookie_token = $_COOKIE['csrf_token'] ?? '';
866 daniel-mar 455
 
456
                if (empty($request_token) || empty($cookie_token) || ($request_token !== $cookie_token)) {
457
                        if (OIDplus::baseConfig()->getValue('DEBUG')) {
458
                                throw new OIDplusException(_L('Missing or wrong CSRF Token: Request %1 vs Cookie %2',
459
                                        isset($_REQUEST['csrf_token']) ? '"'.$_REQUEST['csrf_token'].'"' : 'NULL',
1130 daniel-mar 460
                                        $_COOKIE['csrf_token'] ?? 'NULL'
866 daniel-mar 461
                                ));
462
                        } else {
463
                                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.'));
464
                        }
424 daniel-mar 465
                }
466
        }
329 daniel-mar 467
 
421 daniel-mar 468
        // Generate RA passwords
622 daniel-mar 469
 
1116 daniel-mar 470
        /**
471
         * @param string $password
472
         * @return OIDplusRAAuthInfo
473
         * @throws OIDplusException
474
         */
1186 daniel-mar 475
        public function raGeneratePassword(string $password): OIDplusRAAuthInfo {
1099 daniel-mar 476
                $plugin = OIDplus::getDefaultRaAuthPlugin(true);
1186 daniel-mar 477
                return $plugin->generate($this->raPepperProcessing($password));
421 daniel-mar 478
        }
479
 
480
        // Generate admin password
481
 
585 daniel-mar 482
        /* Nothing here; the admin password will be generated in setup_base.js , purely in the web-browser */
421 daniel-mar 483
 
484
}