Subversion Repositories oidplus

Rev

Rev 1306 | Rev 1315 | 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*/ {
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();
194
                if (!is_null($acs)) {
1301 daniel-mar 195
                        // User is already logged in (a session or JWT exists), so we modify their login status
585 daniel-mar 196
                        $acs->raLoginEx($email, $loginfo);
197
                        $acs->activate();
198
                } else {
1306 daniel-mar 199
                        // 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 200
                        $newAuthStore = new OIDplusAuthContentStoreJWT();
201
                        $newAuthStore->setValue(OIDplusAuthContentStoreJWT::CLAIM_GENERATOR, OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN);
1303 daniel-mar 202
                        $newAuthStore->raLoginEx($email, $loginfo);
203
                        $newAuthStore->activate();
585 daniel-mar 204
                }
205
                $logmsg = "RA '$email' logged in";
206
                if ($origin != '') $logmsg .= " via $origin";
207
                if ($loginfo != '') $logmsg .= " ($loginfo)";
1267 daniel-mar 208
                OIDplus::logger()->log("V2:[OK]RA(%1)", "%2", $email, $logmsg);
585 daniel-mar 209
        }
210
 
1116 daniel-mar 211
        /**
1308 daniel-mar 212
         * "High level" method for RA Logout
1116 daniel-mar 213
         * @param string $email
214
         * @return void
215
         * @throws OIDplusException
216
         */
217
        public function raLogoutEx(string $email) {
585 daniel-mar 218
                $loginfo = '';
219
 
220
                $acs = $this->getAuthContentStore();
221
                if (is_null($acs)) return;
1116 daniel-mar 222
                $acs->raLogoutEx($email, $loginfo);
585 daniel-mar 223
 
1267 daniel-mar 224
                OIDplus::logger()->log("V2:[OK]RA(%1)", "RA '%1' logged out (%2)", $email, $loginfo);
585 daniel-mar 225
 
226
                if (($this->raNumLoggedIn() == 0) && (!$this->isAdminLoggedIn())) {
227
                        // Nobody logged in anymore. Destroy session cookie to make GDPR people happy
228
                        $acs->destroySession();
229
                } else {
230
                        // Get a new token for the remaining users
231
                        $acs->activate();
232
                }
233
        }
234
 
14 daniel-mar 235
        // Admin authentication functions
2 daniel-mar 236
 
1116 daniel-mar 237
        /**
1308 daniel-mar 238
         * "Low level" method for Admin Login
1116 daniel-mar 239
         * @return void
240
         * @throws OIDplusException
241
         */
566 daniel-mar 242
        public function adminLogin() {
585 daniel-mar 243
                $acs = $this->getAuthContentStore();
244
                if (is_null($acs)) return;
1116 daniel-mar 245
                $acs->adminLogin();
2 daniel-mar 246
        }
247
 
1116 daniel-mar 248
        /**
1308 daniel-mar 249
         * "Low level" method for RA Logout
1116 daniel-mar 250
         * @return void
251
         * @throws OIDplusException
252
         */
566 daniel-mar 253
        public function adminLogout() {
585 daniel-mar 254
                $acs = $this->getAuthContentStore();
255
                if (is_null($acs)) return;
1116 daniel-mar 256
                $acs->adminLogout();
2 daniel-mar 257
        }
258
 
1116 daniel-mar 259
        /**
260
         * @param string $password
261
         * @return bool
262
         * @throws OIDplusException
263
         */
264
        public function adminCheckPassword(string $password): bool {
609 daniel-mar 265
                $cfgData = OIDplus::baseConfig()->getValue('ADMIN_PASSWORD', '');
266
                if (empty($cfgData)) {
360 daniel-mar 267
                        throw new OIDplusException(_L('No admin password set in %1','userdata/baseconfig/config.inc.php'));
261 daniel-mar 268
                }
622 daniel-mar 269
 
609 daniel-mar 270
                if (!is_array($cfgData)) {
271
                        $passwordDataArray = array($cfgData);
272
                } else {
1106 daniel-mar 273
                        $passwordDataArray = $cfgData; // Multiple Administrator passwords
609 daniel-mar 274
                }
622 daniel-mar 275
 
609 daniel-mar 276
                foreach ($passwordDataArray as $passwordData) {
1107 daniel-mar 277
                        if (str_starts_with($passwordData, '$')) {
278
                                // Version 3: BCrypt (or any other crypt)
279
                                $ok = password_verify($password, $passwordData);
280
                        } else if (strpos($passwordData, '$') !== false) {
281
                                // Version 2: SHA3-512 with salt
282
                                list($salt, $hash) = explode('$', $passwordData, 2);
283
                                $ok = hash_equals(sha3_512($salt.$password, true), base64_decode($hash));
456 daniel-mar 284
                        } else {
609 daniel-mar 285
                                // Version 1: SHA3-512 without salt
1107 daniel-mar 286
                                $ok = hash_equals(sha3_512($password, true), base64_decode($passwordData));
456 daniel-mar 287
                        }
1107 daniel-mar 288
                        if ($ok) return true;
421 daniel-mar 289
                }
609 daniel-mar 290
 
291
                return false;
2 daniel-mar 292
        }
293
 
1116 daniel-mar 294
        /**
295
         * @return bool
296
         * @throws OIDplusException
297
         */
298
        public function isAdminLoggedIn(): bool {
1308 daniel-mar 299
                $acs = $this->getAuthContentStore();
300
                if (is_null($acs)) return false;
301
                return $acs->isAdminLoggedIn();
2 daniel-mar 302
        }
303
 
1116 daniel-mar 304
        /**
1308 daniel-mar 305
         * "High level" method for Admin Login
1116 daniel-mar 306
         * @param string $origin
307
         * @return void
308
         * @throws OIDplusException
309
         */
1305 daniel-mar 310
        public function adminLoginEx(string $origin='') {
585 daniel-mar 311
                $loginfo = '';
312
                $acs = $this->getAuthContentStore();
313
                if (!is_null($acs)) {
1301 daniel-mar 314
                        // User is already logged in (a session or JWT exists), so we modify their login status
585 daniel-mar 315
                        $acs->adminLoginEx($loginfo);
316
                        $acs->activate();
317
                } else {
1306 daniel-mar 318
                        // 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 319
                        $newAuthStore = new OIDplusAuthContentStoreJWT();
320
                        $newAuthStore->setValue(OIDplusAuthContentStoreJWT::CLAIM_GENERATOR, OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN);
1303 daniel-mar 321
                        $newAuthStore->adminLoginEx($loginfo);
322
                        $newAuthStore->activate();
585 daniel-mar 323
                }
324
                $logmsg = "Admin logged in";
325
                if ($origin != '') $logmsg .= " via $origin";
326
                if ($loginfo != '') $logmsg .= " ($loginfo)";
1267 daniel-mar 327
                OIDplus::logger()->log("V2:[OK]A", "%1", $logmsg);
585 daniel-mar 328
        }
329
 
1116 daniel-mar 330
        /**
1308 daniel-mar 331
         * "High level" method for Admin Logout
1116 daniel-mar 332
         * @return void
333
         * @throws OIDplusException
334
         */
585 daniel-mar 335
        public function adminLogoutEx() {
336
                $loginfo = '';
337
 
338
                $acs = $this->getAuthContentStore();
339
                if (is_null($acs)) return;
1116 daniel-mar 340
                $acs->adminLogoutEx($loginfo);
585 daniel-mar 341
 
342
                if ($this->raNumLoggedIn() == 0) {
343
                        // Nobody here anymore. Destroy the cookie to make GDPR people happy
344
                        $acs->destroySession();
345
                } else {
346
                        // Get a new token for the remaining users
347
                        $acs->activate();
348
                }
349
 
1267 daniel-mar 350
                OIDplus::logger()->log("V2:[OK]A", "Admin logged out (%1)", $loginfo);
585 daniel-mar 351
        }
352
 
1282 daniel-mar 353
        // Authentication keys for generating secrets or validating arguments (e.g. sent by mail)
2 daniel-mar 354
 
1116 daniel-mar 355
        /**
1283 daniel-mar 356
         * @param array|string $data
1116 daniel-mar 357
         * @return string
358
         * @throws OIDplusException
359
         */
1283 daniel-mar 360
        public function makeSecret($data): string {
361
                if (!is_array($data)) $data = [$data];
362
                $data = json_encode($data);
1282 daniel-mar 363
                return sha3_512_hmac($data, 'OIDplus:'.OIDplus::baseConfig()->getValue('SERVER_SECRET'), false);
364
        }
365
 
366
        /**
1283 daniel-mar 367
         * @param array|string $data Arbitary data to be validated later
368
         * @return string A string that need to be validated with validateAuthKey
1282 daniel-mar 369
         * @throws OIDplusException
370
         */
1283 daniel-mar 371
        public function makeAuthKey($data): string {
372
                if (!is_array($data)) $data = [$data];
373
                $ts = time();
374
                $data_ext = [$ts, $data];
375
                $secret = $this->makeSecret($data_ext);
376
                return $ts.'.'.$secret;
2 daniel-mar 377
        }
378
 
1116 daniel-mar 379
        /**
1283 daniel-mar 380
         * @param array|string $data The original data that had been passed to makeAuthKey()
381
         * @param string $auth_key The result from makeAuthKey()
382
         * @param int $valid_secs How many seconds is the auth key valid? (-1 for infinite)
383
         * @return bool True if the key is valid and not expired.
1116 daniel-mar 384
         * @throws OIDplusException
385
         */
1283 daniel-mar 386
        public function validateAuthKey($data, string $auth_key, int $valid_secs=-1): bool {
387
                $auth_key_ary = explode('.', $auth_key, 2);
388
                if (count($auth_key_ary) != 2) return false; // invalid auth key syntax
389
                list($ts, $secret) = $auth_key_ary;
390
                if (!is_numeric($ts)) return false; // invalid auth key syntax
391
                if ($valid_secs >= 0) {
392
                        if (time() > ($ts+$valid_secs)) return false; // expired auth key
393
                }
394
                if (!is_array($data)) $data = [$data];
395
                $data_ext = [(int)$ts, $data];
396
                return hash_equals($this->makeSecret($data_ext), $secret);
2 daniel-mar 397
        }
398
 
329 daniel-mar 399
        // "Veto" functions to force logout state
400
 
1116 daniel-mar 401
        /**
402
         * @return bool
403
         */
404
        protected function forceAllLoggedOut(): bool {
329 daniel-mar 405
                if (isset($_SERVER['SCRIPT_FILENAME']) && (basename($_SERVER['SCRIPT_FILENAME']) == 'sitemap.php')) {
406
                        // The sitemap may not contain any confidential information,
407
                        // even if the user is logged in, because the admin could
408
                        // accidentally copy-paste the sitemap to a
409
                        // search engine control panel while they are logged in
410
                        return true;
411
                } else {
412
                        return false;
413
                }
414
        }
427 daniel-mar 415
 
424 daniel-mar 416
        // CSRF functions
427 daniel-mar 417
 
424 daniel-mar 418
        private $enable_csrf = true;
427 daniel-mar 419
 
1116 daniel-mar 420
        /**
421
         * @return void
422
         */
424 daniel-mar 423
        public function enableCSRF() {
424
                $this->enable_csrf = true;
425
        }
427 daniel-mar 426
 
1116 daniel-mar 427
        /**
428
         * @return void
429
         */
424 daniel-mar 430
        public function disableCSRF() {
431
                $this->enable_csrf = false;
432
        }
427 daniel-mar 433
 
1116 daniel-mar 434
        /**
435
         * @return string
436
         * @throws \Random\RandomException
437
         */
438
        public function genCSRFToken(): string {
1098 daniel-mar 439
                return random_bytes_ex(64, false, false);
424 daniel-mar 440
        }
427 daniel-mar 441
 
1116 daniel-mar 442
        /**
443
         * @return void
444
         * @throws OIDplusException
445
         */
424 daniel-mar 446
        public function checkCSRF() {
427 daniel-mar 447
                if (!$this->enable_csrf) return;
866 daniel-mar 448
 
1130 daniel-mar 449
                $request_token = $_REQUEST['csrf_token'] ?? '';
450
                $cookie_token = $_COOKIE['csrf_token'] ?? '';
866 daniel-mar 451
 
452
                if (empty($request_token) || empty($cookie_token) || ($request_token !== $cookie_token)) {
453
                        if (OIDplus::baseConfig()->getValue('DEBUG')) {
454
                                throw new OIDplusException(_L('Missing or wrong CSRF Token: Request %1 vs Cookie %2',
455
                                        isset($_REQUEST['csrf_token']) ? '"'.$_REQUEST['csrf_token'].'"' : 'NULL',
1130 daniel-mar 456
                                        $_COOKIE['csrf_token'] ?? 'NULL'
866 daniel-mar 457
                                ));
458
                        } else {
459
                                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.'));
460
                        }
424 daniel-mar 461
                }
462
        }
329 daniel-mar 463
 
421 daniel-mar 464
        // Generate RA passwords
622 daniel-mar 465
 
1116 daniel-mar 466
        /**
467
         * @param string $password
468
         * @return OIDplusRAAuthInfo
469
         * @throws OIDplusException
470
         */
1186 daniel-mar 471
        public function raGeneratePassword(string $password): OIDplusRAAuthInfo {
1099 daniel-mar 472
                $plugin = OIDplus::getDefaultRaAuthPlugin(true);
1186 daniel-mar 473
                return $plugin->generate($this->raPepperProcessing($password));
421 daniel-mar 474
        }
475
 
476
        // Generate admin password
477
 
585 daniel-mar 478
        /* Nothing here; the admin password will be generated in setup_base.js , purely in the web-browser */
421 daniel-mar 479
 
480
}