Subversion Repositories oidplus

Rev

Rev 1339 | 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
        /**
1305 daniel-mar 71
         * @return OIDplusAuthContentStoreJWT|null
1116 daniel-mar 72
         * @throws OIDplusException
73
         */
1306 daniel-mar 74
        protected function getAuthContentStore()/*: ?OIDplusAuthContentStoreJWT*/ {
1308 daniel-mar 75
                // Sitemap may not make use of any login/logout state
76
                if ($this->forceAllLoggedOut()) return null;
77
 
585 daniel-mar 78
                // Logged in via JWT
1305 daniel-mar 79
                // (The JWT can come from a login cookie, an REST Authentication Bearer, an AJAX Cookie, or an Automated AJAX Call GET/POST token.)
585 daniel-mar 80
                $tmp = OIDplusAuthContentStoreJWT::getActiveProvider();
81
                if ($tmp) return $tmp;
577 daniel-mar 82
 
585 daniel-mar 83
                // No active session and no JWT token available. User is not logged in.
84
                return null;
577 daniel-mar 85
        }
86
 
1116 daniel-mar 87
        /**
88
         * @param string $name
89
         * @param mixed|null $default
90
         * @return mixed
91
         * @throws OIDplusException
92
         */
93
        public function getExtendedAttribute(string $name, $default=NULL) {
585 daniel-mar 94
                $acs = $this->getAuthContentStore();
95
                if (is_null($acs)) return $default;
96
                return $acs->getValue($name, $default);
577 daniel-mar 97
        }
98
 
566 daniel-mar 99
        // RA authentication functions
2 daniel-mar 100
 
1116 daniel-mar 101
        /**
1308 daniel-mar 102
         * "Low level" method for RA Login
1116 daniel-mar 103
         * @param string $email
104
         * @return void
105
         * @throws OIDplusException
106
         */
107
        public function raLogin(string $email) {
585 daniel-mar 108
                $acs = $this->getAuthContentStore();
109
                if (is_null($acs)) return;
1116 daniel-mar 110
                $acs->raLogin($email);
566 daniel-mar 111
        }
2 daniel-mar 112
 
1116 daniel-mar 113
        /**
1308 daniel-mar 114
         * "Low level" method for RA Logout
1116 daniel-mar 115
         * @param string $email
116
         * @return void
117
         * @throws OIDplusException
118
         */
119
        public function raLogout(string $email) {
585 daniel-mar 120
                $acs = $this->getAuthContentStore();
121
                if (is_null($acs)) return;
1116 daniel-mar 122
                $acs->raLogout($email);
2 daniel-mar 123
        }
124
 
1116 daniel-mar 125
        /**
126
         * @param string $ra_email
127
         * @param string $password
128
         * @return bool
129
         * @throws OIDplusException
130
         */
131
        public function raCheckPassword(string $ra_email, string $password): bool {
329 daniel-mar 132
                $ra = new OIDplusRA($ra_email);
453 daniel-mar 133
 
617 daniel-mar 134
                // Get RA info from RA
459 daniel-mar 135
                $authInfo = $ra->getAuthInfo();
590 daniel-mar 136
                if (!$authInfo) return false; // user not found
622 daniel-mar 137
 
617 daniel-mar 138
                // Ask plugins if they can verify this hash
453 daniel-mar 139
                $plugins = OIDplus::getAuthPlugins();
140
                if (count($plugins) == 0) {
141
                        throw new OIDplusException(_L('No RA authentication plugins found'));
142
                }
143
                foreach ($plugins as $plugin) {
1186 daniel-mar 144
                        if ($plugin->verify($authInfo, $this->raPepperProcessing($password))) return true;
453 daniel-mar 145
                }
146
 
147
                return false;
329 daniel-mar 148
        }
149
 
1116 daniel-mar 150
        /**
151
         * @return int
152
         * @throws OIDplusException
153
         */
154
        public function raNumLoggedIn(): int {
585 daniel-mar 155
                $acs = $this->getAuthContentStore();
156
                if (is_null($acs)) return 0;
157
                return $acs->raNumLoggedIn();
85 daniel-mar 158
        }
159
 
1116 daniel-mar 160
        /**
161
         * @return OIDplusRA[]
162
         * @throws OIDplusException
163
         */
164
        public function loggedInRaList(): array {
1308 daniel-mar 165
                $acs = $this->getAuthContentStore();
166
                if (is_null($acs)) return [];
167
                return $acs->loggedInRaList();
2 daniel-mar 168
        }
169
 
1116 daniel-mar 170
        /**
1267 daniel-mar 171
         * @param string|OIDplusRA $ra
1116 daniel-mar 172
         * @return bool
173
         * @throws OIDplusException
174
         */
1267 daniel-mar 175
        public function isRaLoggedIn($ra): bool {
176
                $email = $ra instanceof OIDplusRA ? $ra->raEmail() : $ra;
585 daniel-mar 177
                $acs = $this->getAuthContentStore();
178
                if (is_null($acs)) return false;
179
                return $acs->isRaLoggedIn($email);
2 daniel-mar 180
        }
181
 
1308 daniel-mar 182
        // "High level" function including logging and checking for valid JWT updates
1116 daniel-mar 183
 
184
        /**
1308 daniel-mar 185
         * "High level" method for RA Login
1116 daniel-mar 186
         * @param string $email
187
         * @param string $origin
188
         * @return void
189
         * @throws OIDplusException
190
         */
1305 daniel-mar 191
        public function raLoginEx(string $email, string $origin='') {
585 daniel-mar 192
                $loginfo = '';
193
                $acs = $this->getAuthContentStore();
1315 daniel-mar 194
                if (is_null($acs)) {
1306 daniel-mar 195
                        // No user is logged in (no JWT exists). We now create a auth content store and activate it (cookies will be set etc.)
1315 daniel-mar 196
                        $acs = new OIDplusAuthContentStoreJWT();
197
                        $acs->setValue(OIDplusAuthContentStoreJWT::CLAIM_GENERATOR, OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN);
585 daniel-mar 198
                }
1315 daniel-mar 199
                $acs->raLoginEx($email, $loginfo);
200
                $acs->activate(); // create or update JWT token
585 daniel-mar 201
                $logmsg = "RA '$email' logged in";
202
                if ($origin != '') $logmsg .= " via $origin";
203
                if ($loginfo != '') $logmsg .= " ($loginfo)";
1267 daniel-mar 204
                OIDplus::logger()->log("V2:[OK]RA(%1)", "%2", $email, $logmsg);
585 daniel-mar 205
        }
206
 
1116 daniel-mar 207
        /**
1308 daniel-mar 208
         * "High level" method for RA Logout
1116 daniel-mar 209
         * @param string $email
210
         * @return void
211
         * @throws OIDplusException
212
         */
213
        public function raLogoutEx(string $email) {
585 daniel-mar 214
                $loginfo = '';
215
 
216
                $acs = $this->getAuthContentStore();
217
                if (is_null($acs)) return;
1116 daniel-mar 218
                $acs->raLogoutEx($email, $loginfo);
585 daniel-mar 219
 
1267 daniel-mar 220
                OIDplus::logger()->log("V2:[OK]RA(%1)", "RA '%1' logged out (%2)", $email, $loginfo);
585 daniel-mar 221
 
222
                if (($this->raNumLoggedIn() == 0) && (!$this->isAdminLoggedIn())) {
223
                        // Nobody logged in anymore. Destroy session cookie to make GDPR people happy
224
                        $acs->destroySession();
225
                } else {
226
                        // Get a new token for the remaining users
227
                        $acs->activate();
228
                }
229
        }
230
 
14 daniel-mar 231
        // Admin authentication functions
2 daniel-mar 232
 
1116 daniel-mar 233
        /**
1308 daniel-mar 234
         * "Low level" method for Admin Login
1116 daniel-mar 235
         * @return void
236
         * @throws OIDplusException
237
         */
566 daniel-mar 238
        public function adminLogin() {
585 daniel-mar 239
                $acs = $this->getAuthContentStore();
240
                if (is_null($acs)) return;
1116 daniel-mar 241
                $acs->adminLogin();
2 daniel-mar 242
        }
243
 
1116 daniel-mar 244
        /**
1308 daniel-mar 245
         * "Low level" method for RA Logout
1116 daniel-mar 246
         * @return void
247
         * @throws OIDplusException
248
         */
566 daniel-mar 249
        public function adminLogout() {
585 daniel-mar 250
                $acs = $this->getAuthContentStore();
251
                if (is_null($acs)) return;
1116 daniel-mar 252
                $acs->adminLogout();
2 daniel-mar 253
        }
254
 
1116 daniel-mar 255
        /**
256
         * @param string $password
257
         * @return bool
258
         * @throws OIDplusException
259
         */
260
        public function adminCheckPassword(string $password): bool {
609 daniel-mar 261
                $cfgData = OIDplus::baseConfig()->getValue('ADMIN_PASSWORD', '');
262
                if (empty($cfgData)) {
360 daniel-mar 263
                        throw new OIDplusException(_L('No admin password set in %1','userdata/baseconfig/config.inc.php'));
261 daniel-mar 264
                }
622 daniel-mar 265
 
609 daniel-mar 266
                if (!is_array($cfgData)) {
267
                        $passwordDataArray = array($cfgData);
268
                } else {
1106 daniel-mar 269
                        $passwordDataArray = $cfgData; // Multiple Administrator passwords
609 daniel-mar 270
                }
622 daniel-mar 271
 
609 daniel-mar 272
                foreach ($passwordDataArray as $passwordData) {
1107 daniel-mar 273
                        if (str_starts_with($passwordData, '$')) {
274
                                // Version 3: BCrypt (or any other crypt)
275
                                $ok = password_verify($password, $passwordData);
276
                        } else if (strpos($passwordData, '$') !== false) {
277
                                // Version 2: SHA3-512 with salt
278
                                list($salt, $hash) = explode('$', $passwordData, 2);
1469 daniel-mar 279
                                $ok = hash_equals(base64_decode($hash), sha3_512($salt.$password, true));
456 daniel-mar 280
                        } else {
609 daniel-mar 281
                                // Version 1: SHA3-512 without salt
1469 daniel-mar 282
                                $ok = hash_equals(base64_decode($passwordData), sha3_512($password, true));
456 daniel-mar 283
                        }
1107 daniel-mar 284
                        if ($ok) return true;
421 daniel-mar 285
                }
609 daniel-mar 286
 
287
                return false;
2 daniel-mar 288
        }
289
 
1116 daniel-mar 290
        /**
291
         * @return bool
292
         * @throws OIDplusException
293
         */
294
        public function isAdminLoggedIn(): bool {
1308 daniel-mar 295
                $acs = $this->getAuthContentStore();
296
                if (is_null($acs)) return false;
297
                return $acs->isAdminLoggedIn();
2 daniel-mar 298
        }
299
 
1116 daniel-mar 300
        /**
1308 daniel-mar 301
         * "High level" method for Admin Login
1116 daniel-mar 302
         * @param string $origin
303
         * @return void
304
         * @throws OIDplusException
305
         */
1305 daniel-mar 306
        public function adminLoginEx(string $origin='') {
585 daniel-mar 307
                $loginfo = '';
308
                $acs = $this->getAuthContentStore();
1315 daniel-mar 309
                if (is_null($acs)) {
1306 daniel-mar 310
                        // No user is logged in (no JWT exists). We now create a auth content store and activate it (cookies will be set etc.)
1315 daniel-mar 311
                        $acs = new OIDplusAuthContentStoreJWT();
312
                        $acs->setValue(OIDplusAuthContentStoreJWT::CLAIM_GENERATOR, OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN);
585 daniel-mar 313
                }
1315 daniel-mar 314
                $acs->adminLoginEx($loginfo);
315
                $acs->activate();
585 daniel-mar 316
                $logmsg = "Admin logged in";
317
                if ($origin != '') $logmsg .= " via $origin";
318
                if ($loginfo != '') $logmsg .= " ($loginfo)";
1267 daniel-mar 319
                OIDplus::logger()->log("V2:[OK]A", "%1", $logmsg);
585 daniel-mar 320
        }
321
 
1116 daniel-mar 322
        /**
1308 daniel-mar 323
         * "High level" method for Admin Logout
1116 daniel-mar 324
         * @return void
325
         * @throws OIDplusException
326
         */
585 daniel-mar 327
        public function adminLogoutEx() {
328
                $loginfo = '';
329
 
330
                $acs = $this->getAuthContentStore();
331
                if (is_null($acs)) return;
1116 daniel-mar 332
                $acs->adminLogoutEx($loginfo);
585 daniel-mar 333
 
334
                if ($this->raNumLoggedIn() == 0) {
335
                        // Nobody here anymore. Destroy the cookie to make GDPR people happy
336
                        $acs->destroySession();
337
                } else {
338
                        // Get a new token for the remaining users
339
                        $acs->activate();
340
                }
341
 
1267 daniel-mar 342
                OIDplus::logger()->log("V2:[OK]A", "Admin logged out (%1)", $loginfo);
585 daniel-mar 343
        }
344
 
1282 daniel-mar 345
        // Authentication keys for generating secrets or validating arguments (e.g. sent by mail)
2 daniel-mar 346
 
1116 daniel-mar 347
        /**
1283 daniel-mar 348
         * @param array|string $data
1116 daniel-mar 349
         * @return string
350
         * @throws OIDplusException
351
         */
1283 daniel-mar 352
        public function makeSecret($data): string {
353
                if (!is_array($data)) $data = [$data];
354
                $data = json_encode($data);
1282 daniel-mar 355
                return sha3_512_hmac($data, 'OIDplus:'.OIDplus::baseConfig()->getValue('SERVER_SECRET'), false);
356
        }
357
 
358
        /**
1283 daniel-mar 359
         * @param array|string $data Arbitary data to be validated later
360
         * @return string A string that need to be validated with validateAuthKey
1282 daniel-mar 361
         * @throws OIDplusException
362
         */
1283 daniel-mar 363
        public function makeAuthKey($data): string {
364
                if (!is_array($data)) $data = [$data];
365
                $ts = time();
366
                $data_ext = [$ts, $data];
367
                $secret = $this->makeSecret($data_ext);
368
                return $ts.'.'.$secret;
2 daniel-mar 369
        }
370
 
1116 daniel-mar 371
        /**
1283 daniel-mar 372
         * @param array|string $data The original data that had been passed to makeAuthKey()
373
         * @param string $auth_key The result from makeAuthKey()
1339 daniel-mar 374
         * @param int $valid_secs How many seconds is the auth key valid? (0 or -1 for infinite)
1283 daniel-mar 375
         * @return bool True if the key is valid and not expired.
1116 daniel-mar 376
         * @throws OIDplusException
377
         */
1283 daniel-mar 378
        public function validateAuthKey($data, string $auth_key, int $valid_secs=-1): bool {
379
                $auth_key_ary = explode('.', $auth_key, 2);
380
                if (count($auth_key_ary) != 2) return false; // invalid auth key syntax
381
                list($ts, $secret) = $auth_key_ary;
382
                if (!is_numeric($ts)) return false; // invalid auth key syntax
1339 daniel-mar 383
                if ($valid_secs > 0) {
1283 daniel-mar 384
                        if (time() > ($ts+$valid_secs)) return false; // expired auth key
385
                }
386
                if (!is_array($data)) $data = [$data];
387
                $data_ext = [(int)$ts, $data];
388
                return hash_equals($this->makeSecret($data_ext), $secret);
2 daniel-mar 389
        }
390
 
329 daniel-mar 391
        // "Veto" functions to force logout state
392
 
1116 daniel-mar 393
        /**
394
         * @return bool
395
         */
396
        protected function forceAllLoggedOut(): bool {
329 daniel-mar 397
                if (isset($_SERVER['SCRIPT_FILENAME']) && (basename($_SERVER['SCRIPT_FILENAME']) == 'sitemap.php')) {
398
                        // The sitemap may not contain any confidential information,
399
                        // even if the user is logged in, because the admin could
400
                        // accidentally copy-paste the sitemap to a
401
                        // search engine control panel while they are logged in
402
                        return true;
403
                } else {
404
                        return false;
405
                }
406
        }
427 daniel-mar 407
 
424 daniel-mar 408
        // CSRF functions
427 daniel-mar 409
 
424 daniel-mar 410
        private $enable_csrf = true;
427 daniel-mar 411
 
1116 daniel-mar 412
        /**
413
         * @return void
414
         */
424 daniel-mar 415
        public function enableCSRF() {
416
                $this->enable_csrf = true;
417
        }
427 daniel-mar 418
 
1116 daniel-mar 419
        /**
420
         * @return void
421
         */
424 daniel-mar 422
        public function disableCSRF() {
423
                $this->enable_csrf = false;
424
        }
427 daniel-mar 425
 
1116 daniel-mar 426
        /**
427
         * @return string
428
         * @throws \Random\RandomException
429
         */
430
        public function genCSRFToken(): string {
1098 daniel-mar 431
                return random_bytes_ex(64, false, false);
424 daniel-mar 432
        }
427 daniel-mar 433
 
1116 daniel-mar 434
        /**
435
         * @return void
436
         * @throws OIDplusException
437
         */
424 daniel-mar 438
        public function checkCSRF() {
427 daniel-mar 439
                if (!$this->enable_csrf) return;
866 daniel-mar 440
 
1130 daniel-mar 441
                $request_token = $_REQUEST['csrf_token'] ?? '';
442
                $cookie_token = $_COOKIE['csrf_token'] ?? '';
866 daniel-mar 443
 
444
                if (empty($request_token) || empty($cookie_token) || ($request_token !== $cookie_token)) {
445
                        if (OIDplus::baseConfig()->getValue('DEBUG')) {
446
                                throw new OIDplusException(_L('Missing or wrong CSRF Token: Request %1 vs Cookie %2',
447
                                        isset($_REQUEST['csrf_token']) ? '"'.$_REQUEST['csrf_token'].'"' : 'NULL',
1130 daniel-mar 448
                                        $_COOKIE['csrf_token'] ?? 'NULL'
866 daniel-mar 449
                                ));
450
                        } else {
451
                                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.'));
452
                        }
424 daniel-mar 453
                }
454
        }
329 daniel-mar 455
 
421 daniel-mar 456
        // Generate RA passwords
622 daniel-mar 457
 
1116 daniel-mar 458
        /**
459
         * @param string $password
460
         * @return OIDplusRAAuthInfo
461
         * @throws OIDplusException
462
         */
1186 daniel-mar 463
        public function raGeneratePassword(string $password): OIDplusRAAuthInfo {
1099 daniel-mar 464
                $plugin = OIDplus::getDefaultRaAuthPlugin(true);
1186 daniel-mar 465
                return $plugin->generate($this->raPepperProcessing($password));
421 daniel-mar 466
        }
467
 
468
        // Generate admin password
469
 
585 daniel-mar 470
        /* Nothing here; the admin password will be generated in setup_base.js , purely in the web-browser */
421 daniel-mar 471
 
472
}