Subversion Repositories oidplus

Rev

Rev 1305 | Rev 1307 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
566 daniel-mar 1
<?php
2
 
3
/*
4
 * OIDplus 2.0
1086 daniel-mar 5
 * Copyright 2019 - 2023 Daniel Marschall, ViaThinkSoft
566 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;
566 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
 
1303 daniel-mar 26
/**
1305 daniel-mar 27
 * Auth content store for JWT tokens (web browser login cookies, Automated AJAX argument, or REST Bearer)
1303 daniel-mar 28
 */
1306 daniel-mar 29
class OIDplusAuthContentStoreJWT implements OIDplusGetterSetterInterface {
566 daniel-mar 30
 
1130 daniel-mar 31
        /**
32
         * Cookie name for the JWT auth token
33
         */
585 daniel-mar 34
        const COOKIE_NAME = 'OIDPLUS_AUTH_JWT';
35
 
1130 daniel-mar 36
        /**
1305 daniel-mar 37
         * Token generator; must be one of OIDplusAuthContentStoreJWT::JWT_GENERATOR_*
38
         */
39
        const CLAIM_GENERATOR = 'urn:oid:1.3.6.1.4.1.37476.2.5.2.7.1';
40
 
41
        /**
42
         * List of logged-in users
43
         */
44
        const CLAIM_LOGIN_LIST = 'urn:oid:1.3.6.1.4.1.37476.2.5.2.7.2';
45
 
46
        /**
47
         * SSH = Server Secret Hash
48
         */
49
        const CLAIM_SSH = 'urn:oid:1.3.6.1.4.1.37476.2.5.2.7.3';
50
 
51
        /**
52
         * IP-Adress limit
53
         */
54
        const CLAIM_LIMIT_IP = 'urn:oid:1.3.6.1.4.1.37476.2.5.2.7.4';
55
 
56
        /**
1130 daniel-mar 57
         * "Automated AJAX" plugin
58
         */
1265 daniel-mar 59
        const JWT_GENERATOR_AJAX   = 10;
1130 daniel-mar 60
        /**
1265 daniel-mar 61
         * "REST API" plugin
62
         */
63
        const JWT_GENERATOR_REST   = 20;
64
        /**
1305 daniel-mar 65
         * Web browser login method
1130 daniel-mar 66
         */
1265 daniel-mar 67
        const JWT_GENERATOR_LOGIN  = 40;
1130 daniel-mar 68
        /**
69
         * "Manually crafted" JWT tokens
70
         */
1265 daniel-mar 71
        const JWT_GENERATOR_MANUAL = 80;
585 daniel-mar 72
 
1116 daniel-mar 73
        /**
74
         * @param int $gen OIDplusAuthContentStoreJWT::JWT_GENERATOR_...
75
         * @param string $sub
76
         * @return string
77
         */
78
        private static function jwtGetBlacklistConfigKey(int $gen, string $sub): string {
1265 daniel-mar 79
                // Note: Needs to be <= 50 characters! If $gen is 2 chars, then the config key is 49 chars long
585 daniel-mar 80
                return 'jwt_blacklist_gen('.$gen.')_sub('.trim(base64_encode(md5($sub,true)),'=').')';
81
        }
82
 
1116 daniel-mar 83
        /**
1265 daniel-mar 84
         * @param int $gen
85
         */
86
        private static function generatorName($gen) {
87
                // Note: The strings are not translated, because the name is used in config keys or logs
88
                if ($gen === self::JWT_GENERATOR_AJAX)   return 'Automated AJAX calls';
89
                if ($gen === self::JWT_GENERATOR_REST)   return 'REST API';
1305 daniel-mar 90
                if ($gen === self::JWT_GENERATOR_LOGIN)  return 'Browser login';
1265 daniel-mar 91
                if ($gen === self::JWT_GENERATOR_MANUAL) return 'Manually created';
92
                return 'Unknown generator';
93
        }
94
 
95
        /**
1116 daniel-mar 96
         * @param int $gen OIDplusAuthContentStoreJWT::JWT_GENERATOR_...
97
         * @param string $sub
98
         * @return void
99
         * @throws OIDplusException
100
         */
101
        public static function jwtBlacklist(int $gen, string $sub) {
585 daniel-mar 102
                $cfg = self::jwtGetBlacklistConfigKey($gen, $sub);
103
                $bl_time = time()-1;
104
 
1265 daniel-mar 105
                $gen_desc = self::generatorName($gen);
585 daniel-mar 106
 
1305 daniel-mar 107
                OIDplus::config()->prepareConfigKey($cfg, "Revoke timestamp of all JWT tokens for $sub with generator $gen ($gen_desc)", "$bl_time", OIDplusConfig::PROTECTION_HIDDEN, function($value) {});
585 daniel-mar 108
                OIDplus::config()->setValue($cfg, $bl_time);
109
        }
110
 
1116 daniel-mar 111
        /**
112
         * @param int $gen OIDplusAuthContentStoreJWT::JWT_GENERATOR_...
113
         * @param string $sub
114
         * @return int
115
         * @throws OIDplusException
116
         */
117
        public static function jwtGetBlacklistTime(int $gen, string $sub): int {
585 daniel-mar 118
                $cfg = self::jwtGetBlacklistConfigKey($gen, $sub);
1116 daniel-mar 119
                return (int)OIDplus::config()->getValue($cfg,0);
585 daniel-mar 120
        }
121
 
1116 daniel-mar 122
        /**
1298 daniel-mar 123
         * We include a hash of the server-secret here (ssh = server-secret-hash), so that the JWT can be invalidated by changing the server-secret
124
         * @return string
125
         * @throws OIDplusException
126
         */
127
        private static function getSsh(): string {
1305 daniel-mar 128
                $hexadecimal_string = OIDplus::authUtils()->makeSecret(['bb1aebd6-fe6a-11ed-a553-3c4a92df8582']);
129
                return base64_encode(pack('H*',$hexadecimal_string));
1298 daniel-mar 130
        }
131
 
132
        /**
1265 daniel-mar 133
         * Do various checks if the token is allowed and not blacklisted
1305 daniel-mar 134
         * @param OIDplusAuthContentStoreJWT $contentProvider
1277 daniel-mar 135
         * @param int|null $validGenerators Bitmask which generators to allow (null = allow all)
1116 daniel-mar 136
         * @return void
137
         * @throws OIDplusException
138
         */
1305 daniel-mar 139
        private static function jwtSecurityCheck(OIDplusAuthContentStoreJWT $contentProvider, int $validGenerators=null) {
585 daniel-mar 140
                // Check if the token is intended for us
699 daniel-mar 141
                if ($contentProvider->getValue('aud','') !== OIDplus::getEditionInfo()['jwtaud']) {
585 daniel-mar 142
                        throw new OIDplusException(_L('Token has wrong audience'));
143
                }
1298 daniel-mar 144
 
1305 daniel-mar 145
                if ($contentProvider->getValue(self::CLAIM_SSH, '') !== self::getSsh()) {
1298 daniel-mar 146
                        throw new OIDplusException(_L('"Server Secret" was changed; therefore the JWT is not valid anymore'));
147
                }
148
 
1305 daniel-mar 149
                $gen = $contentProvider->getValue(self::CLAIM_GENERATOR, -1);
585 daniel-mar 150
 
151
                $has_admin = $contentProvider->isAdminLoggedIn();
152
                $has_ra = $contentProvider->raNumLoggedIn() > 0;
153
 
154
                // Check if the token generator is allowed
155
                if ($gen === self::JWT_GENERATOR_AJAX) {
156
                        if (($has_admin) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_AJAX_ADMIN', true)) {
635 daniel-mar 157
                                // Generator: plugins/viathinksoft/adminPages/910_automated_ajax_calls/OIDplusPageAdminAutomatedAJAXCalls.class.php
585 daniel-mar 158
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_AJAX_ADMIN'));
159
                        }
160
                        if (($has_ra) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_AJAX_USER', true)) {
635 daniel-mar 161
                                // Generator: plugins/viathinksoft/raPages/910_automated_ajax_calls/OIDplusPageRaAutomatedAJAXCalls.class.php
585 daniel-mar 162
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_AJAX_USER'));
163
                        }
164
                }
1265 daniel-mar 165
                else if ($gen === self::JWT_GENERATOR_REST) {
166
                        if (($has_admin) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_REST_ADMIN', true)) {
167
                                // Generator: plugins/viathinksoft/adminPages/911_rest_api/OIDplusPageAdminRestApi.class.php
168
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_REST_ADMIN'));
169
                        }
170
                        if (($has_ra) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_REST_USER', true)) {
171
                                // Generator: plugins/viathinksoft/raPages/911_rest_api/OIDplusPageRaRestApi.class.php
172
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_REST_USER'));
173
                        }
174
                }
585 daniel-mar 175
                else if ($gen === self::JWT_GENERATOR_LOGIN) {
1305 daniel-mar 176
                        // Used for web browser login (use JWT token in a cookie as alternative to PHP session):
585 daniel-mar 177
                        // - No PHP session will be used
178
                        // - Session will not be bound to IP address (therefore, you can switch between mobile/WiFi for example)
179
                        // - No server-side session needed
180
                        if (($has_admin) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) {
181
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_ADMIN'));
182
                        }
183
                        if (($has_ra) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_USER', true)) {
184
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_USER'));
185
                        }
186
                }
187
                else if ($gen === self::JWT_GENERATOR_MANUAL) {
1300 daniel-mar 188
                        // Generator: "hand-crafted" tokens
189
                        if (($has_admin) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_MANUAL_ADMIN', false)) {
190
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_MANUAL_ADMIN'));
585 daniel-mar 191
                        }
1300 daniel-mar 192
                        if (($has_ra) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_MANUAL_USER', false)) {
193
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_MANUAL_USER'));
194
                        }
585 daniel-mar 195
                } else {
196
                        throw new OIDplusException(_L('Token generator %1 not recognized',$gen));
197
                }
198
 
1306 daniel-mar 199
                // Check if token has expired
200
                $exp = $contentProvider->getValue('exp',null);
201
                if (!is_null($exp)) {
202
                        if (time() > $exp) {
203
                                throw new OIDplusException(_L('Token has expired on %1',date('d F Y, H:i:s',$exp)));
204
                        }
205
                }
206
 
585 daniel-mar 207
                // Make sure that the IAT (issued at time) isn't in a blacklisted timeframe
208
                // When an user believes that a token was compromised, then they can blacklist the tokens identified by their "iat" ("Issued at") property
1305 daniel-mar 209
                // When a user logs out of a web browser session, the JWT token will be blacklisted as well
210
                // Small side effect: All web browser login sessions of that user will be revoked then
1281 daniel-mar 211
                $iat = $contentProvider->getValue('iat',0);
212
                if (($iat-120/*leeway 2min*/) > time()) {
213
                        // Token was created in the future. Something is wrong!
214
                        throw new OIDplusException(_L('JWT Token cannot be verified because the server time is wrong'));
215
                }
585 daniel-mar 216
                $sublist = $contentProvider->loggedInRaList();
1281 daniel-mar 217
                $usernames = array();
218
                foreach ($sublist as $sub) {
219
                        $usernames[] = $sub->raEmail();
585 daniel-mar 220
                }
1281 daniel-mar 221
                if ($has_admin) $usernames[] = 'admin';
222
                foreach ($usernames as $username) {
223
                        $bl_time = self::jwtGetBlacklistTime($gen, $username);
585 daniel-mar 224
                        if ($iat <= $bl_time) {
1281 daniel-mar 225
                                // Token is blacklisted (it was created before the last blacklist time)
585 daniel-mar 226
                                throw new OIDplusException(_L('The JWT token was blacklisted on %1. Please generate a new one',date('d F Y, H:i:s',$bl_time)));
227
                        }
228
                }
229
 
230
                // Optional feature: Limit the JWT to a specific IP address
231
                // Currently not used in OIDplus
1305 daniel-mar 232
                $ip = $contentProvider->getValue(self::CLAIM_LIMIT_IP, null);
233
                if (!is_null($ip)) {
585 daniel-mar 234
                        if (isset($_SERVER['REMOTE_ADDR']) && ($ip !== $_SERVER['REMOTE_ADDR'])) {
235
                                throw new OIDplusException(_L('Your IP address is not allowed to use this token'));
236
                        }
237
                }
238
 
1265 daniel-mar 239
                // Checks if JWT are dependent on the generator
1277 daniel-mar 240
                if (!is_null($validGenerators)) {
1265 daniel-mar 241
                        if (($gen & $validGenerators) === 0) {
242
                                throw new OIDplusException(_L('This kind of JWT token (%1) cannot be used in this request type', self::generatorName($gen)));
585 daniel-mar 243
                        }
244
                }
245
        }
246
 
247
        // Override abstract functions
248
 
1116 daniel-mar 249
        /**
1301 daniel-mar 250
         * @var array
251
         */
252
        protected $content = array();
253
 
254
        /**
255
         * @param string $name
256
         * @param mixed|null $default
257
         * @return mixed|null
258
         */
259
        public function getValue(string $name, $default = NULL) {
260
                return $this->content[$name] ?? $default;
261
        }
262
 
263
        /**
264
         * @param string $name
265
         * @param mixed $value
1116 daniel-mar 266
         * @return void
267
         */
1301 daniel-mar 268
        public function setValue(string $name, $value) {
269
                $this->content[$name] = $value;
270
        }
271
 
272
        /**
273
         * @param string $name
274
         * @return bool
275
         */
276
        public function exists(string $name): bool {
277
                return isset($this->content[$name]);
278
        }
279
 
280
        /**
281
         * @param string $name
282
         * @return void
283
         */
284
        public function delete(string $name) {
285
                unset($this->content[$name]);
286
        }
287
 
288
        /**
289
         * @return void
290
         */
585 daniel-mar 291
        public function activate() {
620 daniel-mar 292
                // Send cookie at the end of the HTTP request, in case there are multiple activate() calls
639 daniel-mar 293
                OIDplus::register_shutdown_function(array($this,'activateNow'));
620 daniel-mar 294
        }
295
 
1116 daniel-mar 296
        /**
297
         * @return void
298
         * @throws OIDplusException
299
         */
620 daniel-mar 300
        public function activateNow() {
585 daniel-mar 301
                $token = $this->getJWTToken();
302
                $exp = $this->getValue('exp',0);
303
                OIDplus::cookieUtils()->setcookie(self::COOKIE_NAME, $token, $exp, false);
304
        }
305
 
1116 daniel-mar 306
        /**
307
         * @return void
308
         * @throws OIDplusException
309
         */
585 daniel-mar 310
        public function destroySession() {
311
                OIDplus::cookieUtils()->unsetcookie(self::COOKIE_NAME);
312
        }
313
 
1305 daniel-mar 314
        // RA authentication functions (low-level)
315
 
1116 daniel-mar 316
        /**
317
         * @param string $email
318
         * @return void
1305 daniel-mar 319
         */
320
        public function raLogin(string $email) {
321
                if ($email == 'admin') return;
322
 
323
                $list = $this->getValue(self::CLAIM_LOGIN_LIST, null);
324
                if (is_null($list)) $list = [];
325
                if (!in_array($email, $list)) $list[] = $email;
326
                $this->setValue(self::CLAIM_LOGIN_LIST, $list);
327
        }
328
 
329
        /**
330
         * @return int
331
         */
332
        public function raNumLoggedIn(): int {
333
                return count($this->loggedInRaList());
334
        }
335
 
336
        /**
337
         * @return OIDplusRA[]
338
         */
339
        public function loggedInRaList(): array {
340
                $list = $this->getValue(self::CLAIM_LOGIN_LIST, null);
341
                if (is_null($list)) $list = [];
342
 
343
                $res = array();
344
                foreach (array_unique($list) as $username) {
345
                        if ($username == '') continue; // should not happen
346
                        if ($username == 'admin') continue;
347
                        $res[] = new OIDplusRA($username);
348
                }
349
                return $res;
350
        }
351
 
352
        /**
353
         * @param string $email
354
         * @return bool
355
         */
356
        public function isRaLoggedIn(string $email): bool {
357
                foreach ($this->loggedInRaList() as $ra) {
358
                        if ($email == $ra->raEmail()) return true;
359
                }
360
                return false;
361
        }
362
 
363
        /**
364
         * @param string $email
365
         * @return void
1116 daniel-mar 366
         * @throws OIDplusException
367
         */
368
        public function raLogout(string $email) {
1305 daniel-mar 369
                if ($email == 'admin') return;
370
 
371
                $gen = $this->getValue(self::CLAIM_GENERATOR, -1);
585 daniel-mar 372
                if ($gen >= 0) self::jwtBlacklist($gen, $email);
1305 daniel-mar 373
 
374
                $list = $this->getValue(self::CLAIM_LOGIN_LIST, null);
375
                if (is_null($list)) $list = [];
376
                $key = array_search($email, $list);
377
                if ($key !== false) unset($list[$key]);
378
                $this->setValue(self::CLAIM_LOGIN_LIST, $list);
585 daniel-mar 379
        }
380
 
1116 daniel-mar 381
        /**
382
         * @param string $email
383
         * @param string $loginfo
384
         * @return void
385
         * @throws OIDplusException
386
         */
387
        public function raLogoutEx(string $email, string &$loginfo) {
585 daniel-mar 388
                $this->raLogout($email);
389
                $loginfo = 'from JWT session';
390
        }
391
 
1305 daniel-mar 392
        // Admin authentication functions (low-level)
393
 
1116 daniel-mar 394
        /**
395
         * @return void
1305 daniel-mar 396
         */
397
        public function adminLogin() {
398
                $list = $this->getValue(self::CLAIM_LOGIN_LIST, null);
399
                if (is_null($list)) $list = [];
400
                if (!in_array('admin', $list)) $list[] = 'admin';
401
                $this->setValue(self::CLAIM_LOGIN_LIST, $list);
402
        }
403
 
404
        /**
405
         * @return bool
406
         */
407
        public function isAdminLoggedIn(): bool {
408
                $list = $this->getValue(self::CLAIM_LOGIN_LIST, null);
409
                if (is_null($list)) $list = [];
410
                return in_array('admin', $list);
411
        }
412
 
413
        /**
414
         * @return void
1116 daniel-mar 415
         * @throws OIDplusException
416
         */
585 daniel-mar 417
        public function adminLogout() {
1305 daniel-mar 418
                $gen = $this->getValue(self::CLAIM_GENERATOR, -1);
585 daniel-mar 419
                if ($gen >= 0) self::jwtBlacklist($gen, 'admin');
1305 daniel-mar 420
 
421
                $list = $this->getValue(self::CLAIM_LOGIN_LIST, null);
422
                if (is_null($list)) $list = [];
423
                $key = array_search('admin', $list);
424
                if ($key !== false) unset($list[$key]);
425
                $this->setValue(self::CLAIM_LOGIN_LIST, $list);
585 daniel-mar 426
        }
427
 
1116 daniel-mar 428
        /**
429
         * @param string $loginfo
430
         * @return void
431
         * @throws OIDplusException
432
         */
433
        public function adminLogoutEx(string &$loginfo) {
585 daniel-mar 434
                $this->adminLogout();
435
                $loginfo = 'from JWT session';
436
        }
437
 
620 daniel-mar 438
        private static $contentProvider = null;
1116 daniel-mar 439
 
440
        /**
1305 daniel-mar 441
         * @return OIDplusAuthContentStoreJWT|null
1116 daniel-mar 442
         * @throws OIDplusException
443
         */
1305 daniel-mar 444
        public static function getActiveProvider()/*: ?OIDplusAuthContentStoreJWT*/ {
620 daniel-mar 445
                if (!self::$contentProvider) {
585 daniel-mar 446
 
1265 daniel-mar 447
                        $tmp = null;
448
                        $silent_error = false;
585 daniel-mar 449
 
1265 daniel-mar 450
                        try {
585 daniel-mar 451
 
1265 daniel-mar 452
                                $rel_url = substr($_SERVER['REQUEST_URI'], strlen(OIDplus::webpath(null, OIDplus::PATH_RELATIVE_TO_ROOT)));
453
                                if (str_starts_with($rel_url, 'rest/')) { // <== TODO: Find a way how to move this into the plugin, since REST does not belong to the core.
454
 
455
                                        // REST may only use Bearer Authentication
456
                                        $bearer = getBearerToken();
457
                                        if (!is_null($bearer)) {
458
                                                $silent_error = false;
459
                                                $tmp = new OIDplusAuthContentStoreJWT();
460
                                                $tmp->loadJWT($bearer);
461
                                                self::jwtSecurityCheck($tmp, self::JWT_GENERATOR_REST | self::JWT_GENERATOR_MANUAL);
585 daniel-mar 462
                                        }
1265 daniel-mar 463
 
464
                                } else {
465
 
1305 daniel-mar 466
                                        // A web-visitor (HTML and AJAX, but not REST) can use a JWT Cookie
1265 daniel-mar 467
                                        if (isset($_COOKIE[self::COOKIE_NAME])) {
468
                                                $silent_error = true;
469
                                                $tmp = new OIDplusAuthContentStoreJWT();
470
                                                $tmp->loadJWT($_COOKIE[self::COOKIE_NAME]);
471
                                                self::jwtSecurityCheck($tmp, self::JWT_GENERATOR_LOGIN | self::JWT_GENERATOR_MANUAL);
472
                                        }
473
 
1305 daniel-mar 474
                                        // AJAX may additionally use GET/POST automated AJAX (in addition to the normal web browser login Cookie)
1265 daniel-mar 475
                                        if (isset($_SERVER['SCRIPT_FILENAME']) && (strtolower(basename($_SERVER['SCRIPT_FILENAME'])) !== 'ajax.php')) {
476
                                                if (isset($_POST[self::COOKIE_NAME])) {
477
                                                        $silent_error = false;
478
                                                        $tmp = new OIDplusAuthContentStoreJWT();
479
                                                        $tmp->loadJWT($_POST[self::COOKIE_NAME]);
480
                                                        self::jwtSecurityCheck($tmp, self::JWT_GENERATOR_AJAX | self::JWT_GENERATOR_MANUAL);
481
                                                }
482
                                                if (isset($_GET[self::COOKIE_NAME])) {
483
                                                        $silent_error = false;
484
                                                        $tmp = new OIDplusAuthContentStoreJWT();
485
                                                        $tmp->loadJWT($_GET[self::COOKIE_NAME]);
486
                                                        self::jwtSecurityCheck($tmp, self::JWT_GENERATOR_AJAX | self::JWT_GENERATOR_MANUAL);
487
                                                }
488
                                        }
489
 
585 daniel-mar 490
                                }
491
 
1265 daniel-mar 492
                        } catch (\Exception $e) {
1306 daniel-mar 493
                                if (!$silent_error || OIDplus::baseConfig()->getValue('DEBUG',false)) {
1265 daniel-mar 494
                                        // Most likely an AJAX request. We can throw an Exception
495
                                        throw new OIDplusException(_L('The JWT token was rejected: %1',$e->getMessage()));
496
                                } else {
497
                                        // Most likely an expired Cookie/Login session. We must not throw an Exception, otherwise we will break jsTree
498
                                        OIDplus::cookieUtils()->unsetcookie(self::COOKIE_NAME);
499
                                        return null;
500
                                }
585 daniel-mar 501
                        }
1265 daniel-mar 502
 
503
                        self::$contentProvider = $tmp;
585 daniel-mar 504
                }
505
 
620 daniel-mar 506
                return self::$contentProvider;
585 daniel-mar 507
        }
508
 
1116 daniel-mar 509
        /**
510
         * @param string $email
511
         * @param string $loginfo
512
         * @return void
513
         * @throws OIDplusException
514
         */
515
        public function raLoginEx(string $email, string &$loginfo) {
585 daniel-mar 516
                if (is_null(self::getActiveProvider())) {
517
                        $this->raLogin($email);
518
                        $loginfo = 'into new JWT session';
620 daniel-mar 519
                        self::$contentProvider = $this;
585 daniel-mar 520
                } else {
1305 daniel-mar 521
                        $gen = $this->getValue(self::CLAIM_GENERATOR,-1);
585 daniel-mar 522
                        switch ($gen) {
523
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_AJAX :
1265 daniel-mar 524
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_REST :
585 daniel-mar 525
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_MANUAL :
526
                                        throw new OIDplusException(_L('This kind of JWT token cannot be altered. Therefore you cannot do this action.'));
527
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN :
528
                                        if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_USER', true)) {
1305 daniel-mar 529
                                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_USER'));
585 daniel-mar 530
                                        }
531
                                        break;
532
                                default:
533
                                        assert(false); // This cannot happen because jwtSecurityCheck will check for unknown generators
534
                                        break;
535
                        }
536
                        $this->raLogin($email);
537
                        $loginfo = 'into existing JWT session';
538
                }
1306 daniel-mar 539
                $ttl = OIDplus::baseConfig()->getValue('JWT_TTL_LOGIN_USER', 10*365*24*60*60);
540
                $this->setValue('exp', time()+$ttl); // JWT "exp" attribute
585 daniel-mar 541
        }
542
 
1116 daniel-mar 543
        /**
544
         * @param string $loginfo
545
         * @return void
546
         * @throws OIDplusException
547
         */
548
        public function adminLoginEx(string &$loginfo) {
585 daniel-mar 549
                if (is_null(self::getActiveProvider())) {
550
                        $this->adminLogin();
551
                        $loginfo = 'into new JWT session';
620 daniel-mar 552
                        self::$contentProvider = $this;
585 daniel-mar 553
                } else {
1305 daniel-mar 554
                        $gen = $this->getValue(self::CLAIM_GENERATOR,-1);
585 daniel-mar 555
                        switch ($gen) {
556
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_AJAX :
1265 daniel-mar 557
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_REST :
585 daniel-mar 558
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_MANUAL :
559
                                        throw new OIDplusException(_L('This kind of JWT token cannot be altered. Therefore you cannot do this action.'));
560
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN :
561
                                        if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) {
1305 daniel-mar 562
                                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_ADMIN'));
585 daniel-mar 563
                                        }
564
                                        break;
565
                                default:
566
                                        assert(false); // This cannot happen because jwtSecurityCheck will check for unknown generators
567
                                        break;
568
                        }
569
                        $this->adminLogin();
570
                        $loginfo = 'into existing JWT session';
571
                }
1306 daniel-mar 572
                $ttl = OIDplus::baseConfig()->getValue('JWT_TTL_LOGIN_ADMIN', 10*365*24*60*60);
573
                $this->setValue('exp', time()+$ttl); // JWT "exp" attribute
585 daniel-mar 574
        }
575
 
566 daniel-mar 576
        // Individual functions
577
 
1116 daniel-mar 578
        /**
1265 daniel-mar 579
         * Decode the JWT. In this step, the signature as well as EXP/NBF times will be checked
1116 daniel-mar 580
         * @param string $jwt
581
         * @return void
582
         * @throws OIDplusException
583
         */
584
        public function loadJWT(string $jwt) {
571 daniel-mar 585
                \Firebase\JWT\JWT::$leeway = 60; // leeway in seconds
570 daniel-mar 586
                if (OIDplus::getPkiStatus()) {
830 daniel-mar 587
                        $pubKey = OIDplus::getSystemPublicKey();
1298 daniel-mar 588
                        $k = new \Firebase\JWT\Key($pubKey, 'RS256'); // RSA+SHA256 is hardcoded in getPkiStatus() generation
679 daniel-mar 589
                        $this->content = (array) \Firebase\JWT\JWT::decode($jwt, $k);
570 daniel-mar 590
                } else {
1283 daniel-mar 591
                        $key = OIDplus::authUtils()->makeSecret(['0be35e52-f4ef-11ed-b67e-3c4a92df8582']);
826 daniel-mar 592
                        $key = hash_pbkdf2('sha512', $key, '', 10000, 32/*256bit*/, false);
679 daniel-mar 593
                        $k = new \Firebase\JWT\Key($key, 'HS512'); // HMAC+SHA512 is hardcoded here
594
                        $this->content = (array) \Firebase\JWT\JWT::decode($jwt, $k);
570 daniel-mar 595
                }
566 daniel-mar 596
        }
597
 
1116 daniel-mar 598
        /**
599
         * @return string
600
         * @throws OIDplusException
601
         */
602
        public function getJWTToken(): string {
566 daniel-mar 603
                $payload = $this->content;
1305 daniel-mar 604
                $payload[self::CLAIM_SSH] = self::getSsh(); // SSH = Server Secret Hash
699 daniel-mar 605
                $payload["iss"] = OIDplus::getEditionInfo()['jwtaud'];
606
                $payload["aud"] = OIDplus::getEditionInfo()['jwtaud'];
570 daniel-mar 607
                $payload["jti"] = gen_uuid();
566 daniel-mar 608
                $payload["iat"] = time();
1306 daniel-mar 609
                if (!isset($payload["exp"])) $payload["exp"] = time()+3600/*1h*/;
570 daniel-mar 610
 
1306 daniel-mar 611
                uksort($payload, "strnatcmp"); // this is natsort on the key. Just to make the JWT look nicer.
612
 
570 daniel-mar 613
                if (OIDplus::getPkiStatus()) {
830 daniel-mar 614
                        $privKey = OIDplus::getSystemPrivateKey();
1298 daniel-mar 615
                        return \Firebase\JWT\JWT::encode($payload, $privKey, 'RS256'); // RSA+SHA256 is hardcoded in getPkiStatus() generation
570 daniel-mar 616
                } else {
1283 daniel-mar 617
                        $key = OIDplus::authUtils()->makeSecret(['0be35e52-f4ef-11ed-b67e-3c4a92df8582']);
826 daniel-mar 618
                        $key = hash_pbkdf2('sha512', $key, '', 10000, 32/*256bit*/, false);
679 daniel-mar 619
                        return \Firebase\JWT\JWT::encode($payload, $key, 'HS512'); // HMAC+SHA512 is hardcoded here
570 daniel-mar 620
                }
566 daniel-mar 621
        }
622
 
570 daniel-mar 623
}