Subversion Repositories oidplus

Rev

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