Rev 1301 | Rev 1305 | 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 | /** |
27 | * Auth content store for JWT tokens ("Remember me" cookies, Automated AJAX argument, or REST Bearer) |
||
28 | */ |
||
1301 | daniel-mar | 29 | class OIDplusAuthContentStoreJWT extends OIDplusAuthContentStore { |
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 | /** |
37 | * "Automated AJAX" plugin |
||
38 | */ |
||
1265 | daniel-mar | 39 | const JWT_GENERATOR_AJAX = 10; |
1130 | daniel-mar | 40 | /** |
1265 | daniel-mar | 41 | * "REST API" plugin |
42 | */ |
||
43 | const JWT_GENERATOR_REST = 20; |
||
44 | /** |
||
1130 | daniel-mar | 45 | * "Remember me" login method |
46 | */ |
||
1265 | daniel-mar | 47 | const JWT_GENERATOR_LOGIN = 40; |
1130 | daniel-mar | 48 | /** |
49 | * "Manually crafted" JWT tokens |
||
50 | */ |
||
1265 | daniel-mar | 51 | const JWT_GENERATOR_MANUAL = 80; |
585 | daniel-mar | 52 | |
1116 | daniel-mar | 53 | /** |
54 | * @param int $gen OIDplusAuthContentStoreJWT::JWT_GENERATOR_... |
||
55 | * @param string $sub |
||
56 | * @return string |
||
57 | */ |
||
58 | private static function jwtGetBlacklistConfigKey(int $gen, string $sub): string { |
||
1265 | daniel-mar | 59 | // Note: Needs to be <= 50 characters! If $gen is 2 chars, then the config key is 49 chars long |
585 | daniel-mar | 60 | return 'jwt_blacklist_gen('.$gen.')_sub('.trim(base64_encode(md5($sub,true)),'=').')'; |
61 | } |
||
62 | |||
1116 | daniel-mar | 63 | /** |
1265 | daniel-mar | 64 | * @param int $gen |
65 | */ |
||
66 | private static function generatorName($gen) { |
||
67 | // Note: The strings are not translated, because the name is used in config keys or logs |
||
68 | if ($gen === self::JWT_GENERATOR_AJAX) return 'Automated AJAX calls'; |
||
69 | if ($gen === self::JWT_GENERATOR_REST) return 'REST API'; |
||
70 | if ($gen === self::JWT_GENERATOR_LOGIN) return 'Login ("Remember me")'; |
||
71 | if ($gen === self::JWT_GENERATOR_MANUAL) return 'Manually created'; |
||
72 | return 'Unknown generator'; |
||
73 | } |
||
74 | |||
75 | /** |
||
1116 | daniel-mar | 76 | * @param int $gen OIDplusAuthContentStoreJWT::JWT_GENERATOR_... |
77 | * @param string $sub |
||
78 | * @return void |
||
79 | * @throws OIDplusException |
||
80 | */ |
||
81 | public static function jwtBlacklist(int $gen, string $sub) { |
||
585 | daniel-mar | 82 | $cfg = self::jwtGetBlacklistConfigKey($gen, $sub); |
83 | $bl_time = time()-1; |
||
84 | |||
1265 | daniel-mar | 85 | $gen_desc = self::generatorName($gen); |
585 | daniel-mar | 86 | |
1116 | daniel-mar | 87 | 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 | 88 | OIDplus::config()->setValue($cfg, $bl_time); |
89 | } |
||
90 | |||
1116 | daniel-mar | 91 | /** |
92 | * @param int $gen OIDplusAuthContentStoreJWT::JWT_GENERATOR_... |
||
93 | * @param string $sub |
||
94 | * @return int |
||
95 | * @throws OIDplusException |
||
96 | */ |
||
97 | public static function jwtGetBlacklistTime(int $gen, string $sub): int { |
||
585 | daniel-mar | 98 | $cfg = self::jwtGetBlacklistConfigKey($gen, $sub); |
1116 | daniel-mar | 99 | return (int)OIDplus::config()->getValue($cfg,0); |
585 | daniel-mar | 100 | } |
101 | |||
1116 | daniel-mar | 102 | /** |
1298 | daniel-mar | 103 | * 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 |
104 | * @return string |
||
105 | * @throws OIDplusException |
||
106 | */ |
||
107 | private static function getSsh(): string { |
||
108 | return OIDplus::authUtils()->makeSecret(['bb1aebd6-fe6a-11ed-a553-3c4a92df8582']); |
||
109 | } |
||
110 | |||
111 | /** |
||
1265 | daniel-mar | 112 | * Do various checks if the token is allowed and not blacklisted |
1116 | daniel-mar | 113 | * @param OIDplusAuthContentStore $contentProvider |
1277 | daniel-mar | 114 | * @param int|null $validGenerators Bitmask which generators to allow (null = allow all) |
1116 | daniel-mar | 115 | * @return void |
116 | * @throws OIDplusException |
||
117 | */ |
||
1277 | daniel-mar | 118 | private static function jwtSecurityCheck(OIDplusAuthContentStore $contentProvider, int $validGenerators=null) { |
585 | daniel-mar | 119 | // Check if the token is intended for us |
699 | daniel-mar | 120 | if ($contentProvider->getValue('aud','') !== OIDplus::getEditionInfo()['jwtaud']) { |
585 | daniel-mar | 121 | throw new OIDplusException(_L('Token has wrong audience')); |
122 | } |
||
1298 | daniel-mar | 123 | |
124 | if ($contentProvider->getValue('oidplus_ssh', '') !== self::getSsh()) { |
||
125 | throw new OIDplusException(_L('"Server Secret" was changed; therefore the JWT is not valid anymore')); |
||
126 | } |
||
127 | |||
585 | daniel-mar | 128 | $gen = $contentProvider->getValue('oidplus_generator', -1); |
129 | |||
130 | $has_admin = $contentProvider->isAdminLoggedIn(); |
||
131 | $has_ra = $contentProvider->raNumLoggedIn() > 0; |
||
132 | |||
133 | // Check if the token generator is allowed |
||
134 | if ($gen === self::JWT_GENERATOR_AJAX) { |
||
135 | if (($has_admin) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_AJAX_ADMIN', true)) { |
||
635 | daniel-mar | 136 | // Generator: plugins/viathinksoft/adminPages/910_automated_ajax_calls/OIDplusPageAdminAutomatedAJAXCalls.class.php |
585 | daniel-mar | 137 | throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_AJAX_ADMIN')); |
138 | } |
||
139 | if (($has_ra) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_AJAX_USER', true)) { |
||
635 | daniel-mar | 140 | // Generator: plugins/viathinksoft/raPages/910_automated_ajax_calls/OIDplusPageRaAutomatedAJAXCalls.class.php |
585 | daniel-mar | 141 | throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_AJAX_USER')); |
142 | } |
||
143 | } |
||
1265 | daniel-mar | 144 | else if ($gen === self::JWT_GENERATOR_REST) { |
145 | if (($has_admin) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_REST_ADMIN', true)) { |
||
146 | // Generator: plugins/viathinksoft/adminPages/911_rest_api/OIDplusPageAdminRestApi.class.php |
||
147 | throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_REST_ADMIN')); |
||
148 | } |
||
149 | if (($has_ra) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_REST_USER', true)) { |
||
150 | // Generator: plugins/viathinksoft/raPages/911_rest_api/OIDplusPageRaRestApi.class.php |
||
151 | throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_REST_USER')); |
||
152 | } |
||
153 | } |
||
585 | daniel-mar | 154 | else if ($gen === self::JWT_GENERATOR_LOGIN) { |
155 | // Used for feature "Remember me" (use JWT token in a cookie as alternative to PHP session): |
||
156 | // - No PHP session will be used |
||
157 | // - Session will not be bound to IP address (therefore, you can switch between mobile/WiFi for example) |
||
158 | // - No server-side session needed |
||
159 | if (($has_admin) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) { |
||
160 | throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_ADMIN')); |
||
161 | } |
||
162 | if (($has_ra) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_USER', true)) { |
||
163 | throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_USER')); |
||
164 | } |
||
165 | } |
||
166 | else if ($gen === self::JWT_GENERATOR_MANUAL) { |
||
1300 | daniel-mar | 167 | // Generator: "hand-crafted" tokens |
168 | if (($has_admin) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_MANUAL_ADMIN', false)) { |
||
169 | throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_MANUAL_ADMIN')); |
||
585 | daniel-mar | 170 | } |
1300 | daniel-mar | 171 | if (($has_ra) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_MANUAL_USER', false)) { |
172 | throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_MANUAL_USER')); |
||
173 | } |
||
585 | daniel-mar | 174 | } else { |
175 | throw new OIDplusException(_L('Token generator %1 not recognized',$gen)); |
||
176 | } |
||
177 | |||
178 | // Make sure that the IAT (issued at time) isn't in a blacklisted timeframe |
||
179 | // When an user believes that a token was compromised, then they can blacklist the tokens identified by their "iat" ("Issued at") property |
||
180 | // When a user logs out of a "remember me" session, the JWT token will be blacklisted as well |
||
181 | // Small side effect: All "remember me" sessions of that user will be revoked then |
||
1281 | daniel-mar | 182 | $iat = $contentProvider->getValue('iat',0); |
183 | if (($iat-120/*leeway 2min*/) > time()) { |
||
184 | // Token was created in the future. Something is wrong! |
||
185 | throw new OIDplusException(_L('JWT Token cannot be verified because the server time is wrong')); |
||
186 | } |
||
585 | daniel-mar | 187 | $sublist = $contentProvider->loggedInRaList(); |
1281 | daniel-mar | 188 | $usernames = array(); |
189 | foreach ($sublist as $sub) { |
||
190 | $usernames[] = $sub->raEmail(); |
||
585 | daniel-mar | 191 | } |
1281 | daniel-mar | 192 | if ($has_admin) $usernames[] = 'admin'; |
193 | foreach ($usernames as $username) { |
||
194 | $bl_time = self::jwtGetBlacklistTime($gen, $username); |
||
585 | daniel-mar | 195 | if ($iat <= $bl_time) { |
1281 | daniel-mar | 196 | // Token is blacklisted (it was created before the last blacklist time) |
585 | daniel-mar | 197 | 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))); |
198 | } |
||
199 | } |
||
200 | |||
201 | // Optional feature: Limit the JWT to a specific IP address |
||
202 | // Currently not used in OIDplus |
||
1300 | daniel-mar | 203 | $ip = $contentProvider->getValue('oidplus_limit_ip',''); |
585 | daniel-mar | 204 | if ($ip !== '') { |
205 | if (isset($_SERVER['REMOTE_ADDR']) && ($ip !== $_SERVER['REMOTE_ADDR'])) { |
||
206 | throw new OIDplusException(_L('Your IP address is not allowed to use this token')); |
||
207 | } |
||
208 | } |
||
209 | |||
1265 | daniel-mar | 210 | // Checks if JWT are dependent on the generator |
1277 | daniel-mar | 211 | if (!is_null($validGenerators)) { |
1265 | daniel-mar | 212 | if (($gen & $validGenerators) === 0) { |
213 | throw new OIDplusException(_L('This kind of JWT token (%1) cannot be used in this request type', self::generatorName($gen))); |
||
585 | daniel-mar | 214 | } |
215 | } |
||
216 | } |
||
217 | |||
218 | // Override abstract functions |
||
219 | |||
1116 | daniel-mar | 220 | /** |
1301 | daniel-mar | 221 | * @var array |
222 | */ |
||
223 | protected $content = array(); |
||
224 | |||
225 | /** |
||
226 | * @param string $name |
||
227 | * @param mixed|null $default |
||
228 | * @return mixed|null |
||
229 | */ |
||
230 | public function getValue(string $name, $default = NULL) { |
||
231 | return $this->content[$name] ?? $default; |
||
232 | } |
||
233 | |||
234 | /** |
||
235 | * @param string $name |
||
236 | * @param mixed $value |
||
1116 | daniel-mar | 237 | * @return void |
238 | */ |
||
1301 | daniel-mar | 239 | public function setValue(string $name, $value) { |
240 | $this->content[$name] = $value; |
||
241 | } |
||
242 | |||
243 | /** |
||
244 | * @param string $name |
||
245 | * @return bool |
||
246 | */ |
||
247 | public function exists(string $name): bool { |
||
248 | return isset($this->content[$name]); |
||
249 | } |
||
250 | |||
251 | /** |
||
252 | * @param string $name |
||
253 | * @return void |
||
254 | */ |
||
255 | public function delete(string $name) { |
||
256 | unset($this->content[$name]); |
||
257 | } |
||
258 | |||
259 | /** |
||
260 | * @return void |
||
261 | */ |
||
585 | daniel-mar | 262 | public function activate() { |
620 | daniel-mar | 263 | // Send cookie at the end of the HTTP request, in case there are multiple activate() calls |
639 | daniel-mar | 264 | OIDplus::register_shutdown_function(array($this,'activateNow')); |
620 | daniel-mar | 265 | } |
266 | |||
1116 | daniel-mar | 267 | /** |
268 | * @return void |
||
269 | * @throws OIDplusException |
||
270 | */ |
||
620 | daniel-mar | 271 | public function activateNow() { |
585 | daniel-mar | 272 | $token = $this->getJWTToken(); |
273 | $exp = $this->getValue('exp',0); |
||
274 | OIDplus::cookieUtils()->setcookie(self::COOKIE_NAME, $token, $exp, false); |
||
275 | } |
||
276 | |||
1116 | daniel-mar | 277 | /** |
278 | * @return void |
||
279 | * @throws OIDplusException |
||
280 | */ |
||
585 | daniel-mar | 281 | public function destroySession() { |
282 | OIDplus::cookieUtils()->unsetcookie(self::COOKIE_NAME); |
||
283 | } |
||
284 | |||
1116 | daniel-mar | 285 | /** |
286 | * @param string $email |
||
287 | * @return void |
||
288 | * @throws OIDplusException |
||
289 | */ |
||
290 | public function raLogout(string $email) { |
||
585 | daniel-mar | 291 | $gen = $this->getValue('oidplus_generator', -1); |
292 | if ($gen >= 0) self::jwtBlacklist($gen, $email); |
||
293 | parent::raLogout($email); |
||
294 | } |
||
295 | |||
1116 | daniel-mar | 296 | /** |
297 | * @param string $email |
||
298 | * @param string $loginfo |
||
299 | * @return void |
||
300 | * @throws OIDplusException |
||
301 | */ |
||
302 | public function raLogoutEx(string $email, string &$loginfo) { |
||
585 | daniel-mar | 303 | $this->raLogout($email); |
304 | $loginfo = 'from JWT session'; |
||
305 | } |
||
306 | |||
1116 | daniel-mar | 307 | /** |
308 | * @return void |
||
309 | * @throws OIDplusException |
||
310 | */ |
||
585 | daniel-mar | 311 | public function adminLogout() { |
312 | $gen = $this->getValue('oidplus_generator', -1); |
||
313 | if ($gen >= 0) self::jwtBlacklist($gen, 'admin'); |
||
314 | parent::adminLogout(); |
||
315 | } |
||
316 | |||
1116 | daniel-mar | 317 | /** |
318 | * @param string $loginfo |
||
319 | * @return void |
||
320 | * @throws OIDplusException |
||
321 | */ |
||
322 | public function adminLogoutEx(string &$loginfo) { |
||
585 | daniel-mar | 323 | $this->adminLogout(); |
324 | $loginfo = 'from JWT session'; |
||
325 | } |
||
326 | |||
620 | daniel-mar | 327 | private static $contentProvider = null; |
1116 | daniel-mar | 328 | |
329 | /** |
||
330 | * @return OIDplusAuthContentStore|null |
||
331 | * @throws OIDplusException |
||
332 | */ |
||
333 | public static function getActiveProvider()/*: ?OIDplusAuthContentStore*/ { |
||
620 | daniel-mar | 334 | if (!self::$contentProvider) { |
585 | daniel-mar | 335 | |
1265 | daniel-mar | 336 | $tmp = null; |
337 | $silent_error = false; |
||
585 | daniel-mar | 338 | |
1265 | daniel-mar | 339 | try { |
585 | daniel-mar | 340 | |
1265 | daniel-mar | 341 | $rel_url = substr($_SERVER['REQUEST_URI'], strlen(OIDplus::webpath(null, OIDplus::PATH_RELATIVE_TO_ROOT))); |
342 | 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. |
||
343 | |||
344 | // REST may only use Bearer Authentication |
||
345 | $bearer = getBearerToken(); |
||
346 | if (!is_null($bearer)) { |
||
347 | $silent_error = false; |
||
348 | $tmp = new OIDplusAuthContentStoreJWT(); |
||
349 | $tmp->loadJWT($bearer); |
||
350 | self::jwtSecurityCheck($tmp, self::JWT_GENERATOR_REST | self::JWT_GENERATOR_MANUAL); |
||
585 | daniel-mar | 351 | } |
1265 | daniel-mar | 352 | |
353 | } else { |
||
354 | |||
355 | // A web-visitor (HTML and AJAX, but not REST) can use a JWT "remember me" Cookie |
||
356 | if (isset($_COOKIE[self::COOKIE_NAME])) { |
||
357 | $silent_error = true; |
||
358 | $tmp = new OIDplusAuthContentStoreJWT(); |
||
359 | $tmp->loadJWT($_COOKIE[self::COOKIE_NAME]); |
||
360 | self::jwtSecurityCheck($tmp, self::JWT_GENERATOR_LOGIN | self::JWT_GENERATOR_MANUAL); |
||
361 | } |
||
362 | |||
363 | // AJAX may additionally use GET/POST automated AJAX (in addition to the normal JWT "remember me" Cookie) |
||
364 | if (isset($_SERVER['SCRIPT_FILENAME']) && (strtolower(basename($_SERVER['SCRIPT_FILENAME'])) !== 'ajax.php')) { |
||
365 | if (isset($_POST[self::COOKIE_NAME])) { |
||
366 | $silent_error = false; |
||
367 | $tmp = new OIDplusAuthContentStoreJWT(); |
||
368 | $tmp->loadJWT($_POST[self::COOKIE_NAME]); |
||
369 | self::jwtSecurityCheck($tmp, self::JWT_GENERATOR_AJAX | self::JWT_GENERATOR_MANUAL); |
||
370 | } |
||
371 | if (isset($_GET[self::COOKIE_NAME])) { |
||
372 | $silent_error = false; |
||
373 | $tmp = new OIDplusAuthContentStoreJWT(); |
||
374 | $tmp->loadJWT($_GET[self::COOKIE_NAME]); |
||
375 | self::jwtSecurityCheck($tmp, self::JWT_GENERATOR_AJAX | self::JWT_GENERATOR_MANUAL); |
||
376 | } |
||
377 | } |
||
378 | |||
585 | daniel-mar | 379 | } |
380 | |||
1265 | daniel-mar | 381 | } catch (\Exception $e) { |
382 | if (!$silent_error) { |
||
383 | // Most likely an AJAX request. We can throw an Exception |
||
384 | throw new OIDplusException(_L('The JWT token was rejected: %1',$e->getMessage())); |
||
385 | } else { |
||
386 | // Most likely an expired Cookie/Login session. We must not throw an Exception, otherwise we will break jsTree |
||
387 | OIDplus::cookieUtils()->unsetcookie(self::COOKIE_NAME); |
||
388 | return null; |
||
389 | } |
||
585 | daniel-mar | 390 | } |
1265 | daniel-mar | 391 | |
392 | self::$contentProvider = $tmp; |
||
585 | daniel-mar | 393 | } |
394 | |||
620 | daniel-mar | 395 | return self::$contentProvider; |
585 | daniel-mar | 396 | } |
397 | |||
1116 | daniel-mar | 398 | /** |
399 | * @param string $email |
||
400 | * @param string $loginfo |
||
401 | * @return void |
||
402 | * @throws OIDplusException |
||
403 | */ |
||
404 | public function raLoginEx(string $email, string &$loginfo) { |
||
585 | daniel-mar | 405 | if (is_null(self::getActiveProvider())) { |
406 | $this->raLogin($email); |
||
407 | $loginfo = 'into new JWT session'; |
||
620 | daniel-mar | 408 | self::$contentProvider = $this; |
585 | daniel-mar | 409 | } else { |
410 | $gen = $this->getValue('oidplus_generator',-1); |
||
411 | switch ($gen) { |
||
412 | case OIDplusAuthContentStoreJWT::JWT_GENERATOR_AJAX : |
||
1265 | daniel-mar | 413 | case OIDplusAuthContentStoreJWT::JWT_GENERATOR_REST : |
585 | daniel-mar | 414 | case OIDplusAuthContentStoreJWT::JWT_GENERATOR_MANUAL : |
415 | throw new OIDplusException(_L('This kind of JWT token cannot be altered. Therefore you cannot do this action.')); |
||
416 | case OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN : |
||
417 | if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_USER', true)) { |
||
418 | throw new OIDplusException(_L('You cannot add this login credential to your existing "remember me" session. You need to log-out first.')); |
||
419 | } |
||
420 | break; |
||
421 | default: |
||
422 | assert(false); // This cannot happen because jwtSecurityCheck will check for unknown generators |
||
423 | break; |
||
424 | } |
||
425 | $this->raLogin($email); |
||
426 | $loginfo = 'into existing JWT session'; |
||
427 | } |
||
428 | } |
||
429 | |||
1116 | daniel-mar | 430 | /** |
431 | * @param string $loginfo |
||
432 | * @return void |
||
433 | * @throws OIDplusException |
||
434 | */ |
||
435 | public function adminLoginEx(string &$loginfo) { |
||
585 | daniel-mar | 436 | if (is_null(self::getActiveProvider())) { |
437 | $this->adminLogin(); |
||
438 | $loginfo = 'into new JWT session'; |
||
620 | daniel-mar | 439 | self::$contentProvider = $this; |
585 | daniel-mar | 440 | } else { |
441 | $gen = $this->getValue('oidplus_generator',-1); |
||
442 | switch ($gen) { |
||
443 | case OIDplusAuthContentStoreJWT::JWT_GENERATOR_AJAX : |
||
1265 | daniel-mar | 444 | case OIDplusAuthContentStoreJWT::JWT_GENERATOR_REST : |
585 | daniel-mar | 445 | case OIDplusAuthContentStoreJWT::JWT_GENERATOR_MANUAL : |
446 | throw new OIDplusException(_L('This kind of JWT token cannot be altered. Therefore you cannot do this action.')); |
||
447 | case OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN : |
||
448 | if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) { |
||
449 | throw new OIDplusException(_L('You cannot add this login credential to your existing "remember me" session. You need to log-out first.')); |
||
450 | } |
||
451 | break; |
||
452 | default: |
||
453 | assert(false); // This cannot happen because jwtSecurityCheck will check for unknown generators |
||
454 | break; |
||
455 | } |
||
456 | $this->adminLogin(); |
||
457 | $loginfo = 'into existing JWT session'; |
||
458 | } |
||
459 | } |
||
460 | |||
566 | daniel-mar | 461 | // Individual functions |
462 | |||
1116 | daniel-mar | 463 | /** |
1265 | daniel-mar | 464 | * Decode the JWT. In this step, the signature as well as EXP/NBF times will be checked |
1116 | daniel-mar | 465 | * @param string $jwt |
466 | * @return void |
||
467 | * @throws OIDplusException |
||
468 | */ |
||
469 | public function loadJWT(string $jwt) { |
||
571 | daniel-mar | 470 | \Firebase\JWT\JWT::$leeway = 60; // leeway in seconds |
570 | daniel-mar | 471 | if (OIDplus::getPkiStatus()) { |
830 | daniel-mar | 472 | $pubKey = OIDplus::getSystemPublicKey(); |
1298 | daniel-mar | 473 | $k = new \Firebase\JWT\Key($pubKey, 'RS256'); // RSA+SHA256 is hardcoded in getPkiStatus() generation |
679 | daniel-mar | 474 | $this->content = (array) \Firebase\JWT\JWT::decode($jwt, $k); |
570 | daniel-mar | 475 | } else { |
1283 | daniel-mar | 476 | $key = OIDplus::authUtils()->makeSecret(['0be35e52-f4ef-11ed-b67e-3c4a92df8582']); |
826 | daniel-mar | 477 | $key = hash_pbkdf2('sha512', $key, '', 10000, 32/*256bit*/, false); |
679 | daniel-mar | 478 | $k = new \Firebase\JWT\Key($key, 'HS512'); // HMAC+SHA512 is hardcoded here |
479 | $this->content = (array) \Firebase\JWT\JWT::decode($jwt, $k); |
||
570 | daniel-mar | 480 | } |
566 | daniel-mar | 481 | } |
482 | |||
1116 | daniel-mar | 483 | /** |
484 | * @return string |
||
485 | * @throws OIDplusException |
||
486 | */ |
||
487 | public function getJWTToken(): string { |
||
566 | daniel-mar | 488 | $payload = $this->content; |
1300 | daniel-mar | 489 | $payload["oidplus_ssh"] = self::getSsh(); // SSH = Server Secret Hash |
699 | daniel-mar | 490 | $payload["iss"] = OIDplus::getEditionInfo()['jwtaud']; |
491 | $payload["aud"] = OIDplus::getEditionInfo()['jwtaud']; |
||
570 | daniel-mar | 492 | $payload["jti"] = gen_uuid(); |
566 | daniel-mar | 493 | $payload["iat"] = time(); |
570 | daniel-mar | 494 | |
495 | if (OIDplus::getPkiStatus()) { |
||
830 | daniel-mar | 496 | $privKey = OIDplus::getSystemPrivateKey(); |
1298 | daniel-mar | 497 | return \Firebase\JWT\JWT::encode($payload, $privKey, 'RS256'); // RSA+SHA256 is hardcoded in getPkiStatus() generation |
570 | daniel-mar | 498 | } else { |
1283 | daniel-mar | 499 | $key = OIDplus::authUtils()->makeSecret(['0be35e52-f4ef-11ed-b67e-3c4a92df8582']); |
826 | daniel-mar | 500 | $key = hash_pbkdf2('sha512', $key, '', 10000, 32/*256bit*/, false); |
679 | daniel-mar | 501 | return \Firebase\JWT\JWT::encode($payload, $key, 'HS512'); // HMAC+SHA512 is hardcoded here |
570 | daniel-mar | 502 | } |
566 | daniel-mar | 503 | } |
504 | |||
570 | daniel-mar | 505 | } |