Subversion Repositories oidplus

Rev

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