Rev 1130 | Rev 1277 | Go to most recent revision | Show entire file | Regard whitespace | Details | Blame | Last modification | View Log | RSS feed
Rev 1130 | Rev 1265 | ||
---|---|---|---|
Line 31... | Line 31... | ||
31 | const COOKIE_NAME = 'OIDPLUS_AUTH_JWT'; |
31 | const COOKIE_NAME = 'OIDPLUS_AUTH_JWT'; |
32 | 32 | ||
33 | /** |
33 | /** |
34 | * "Automated AJAX" plugin |
34 | * "Automated AJAX" plugin |
35 | */ |
35 | */ |
36 | const JWT_GENERATOR_AJAX = 0; |
36 | const JWT_GENERATOR_AJAX = 10; |
- | 37 | /** |
|
- | 38 | * "REST API" plugin |
|
- | 39 | */ |
|
- | 40 | const JWT_GENERATOR_REST = 20; |
|
37 | /** |
41 | /** |
38 | * "Remember me" login method |
42 | * "Remember me" login method |
39 | */ |
43 | */ |
40 | const JWT_GENERATOR_LOGIN = 1; |
44 | const JWT_GENERATOR_LOGIN = 40; |
41 | /** |
45 | /** |
42 | * "Manually crafted" JWT tokens |
46 | * "Manually crafted" JWT tokens |
43 | */ |
47 | */ |
44 | const JWT_GENERATOR_MANUAL = 2; |
48 | const JWT_GENERATOR_MANUAL = 80; |
45 | 49 | ||
46 | /** |
50 | /** |
47 | * @param int $gen OIDplusAuthContentStoreJWT::JWT_GENERATOR_... |
51 | * @param int $gen OIDplusAuthContentStoreJWT::JWT_GENERATOR_... |
48 | * @param string $sub |
52 | * @param string $sub |
49 | * @return string |
53 | * @return string |
50 | */ |
54 | */ |
51 | private static function jwtGetBlacklistConfigKey(int $gen, string $sub): string { |
55 | private static function jwtGetBlacklistConfigKey(int $gen, string $sub): string { |
52 | // Note: Needs to be <= 50 characters! |
56 | // Note: Needs to be <= 50 characters! If $gen is 2 chars, then the config key is 49 chars long |
53 | return 'jwt_blacklist_gen('.$gen.')_sub('.trim(base64_encode(md5($sub,true)),'=').')'; |
57 | return 'jwt_blacklist_gen('.$gen.')_sub('.trim(base64_encode(md5($sub,true)),'=').')'; |
54 | } |
58 | } |
55 | 59 | ||
56 | /** |
60 | /** |
- | 61 | * @param int $gen |
|
- | 62 | */ |
|
- | 63 | private static function generatorName($gen) { |
|
- | 64 | // Note: The strings are not translated, because the name is used in config keys or logs |
|
- | 65 | if ($gen === self::JWT_GENERATOR_AJAX) return 'Automated AJAX calls'; |
|
- | 66 | if ($gen === self::JWT_GENERATOR_REST) return 'REST API'; |
|
- | 67 | if ($gen === self::JWT_GENERATOR_LOGIN) return 'Login ("Remember me")'; |
|
- | 68 | if ($gen === self::JWT_GENERATOR_MANUAL) return 'Manually created'; |
|
- | 69 | return 'Unknown generator'; |
|
- | 70 | } |
|
- | 71 | ||
- | 72 | /** |
|
57 | * @param int $gen OIDplusAuthContentStoreJWT::JWT_GENERATOR_... |
73 | * @param int $gen OIDplusAuthContentStoreJWT::JWT_GENERATOR_... |
58 | * @param string $sub |
74 | * @param string $sub |
59 | * @return void |
75 | * @return void |
60 | * @throws OIDplusException |
76 | * @throws OIDplusException |
61 | */ |
77 | */ |
62 | public static function jwtBlacklist(int $gen, string $sub) { |
78 | public static function jwtBlacklist(int $gen, string $sub) { |
63 | $cfg = self::jwtGetBlacklistConfigKey($gen, $sub); |
79 | $cfg = self::jwtGetBlacklistConfigKey($gen, $sub); |
64 | $bl_time = time()-1; |
80 | $bl_time = time()-1; |
65 | 81 | ||
66 | $gen_desc = 'Unknown'; |
82 | $gen_desc = self::generatorName($gen); |
67 | if ($gen === self::JWT_GENERATOR_AJAX) $gen_desc = 'Automated AJAX calls'; |
- | |
68 | if ($gen === self::JWT_GENERATOR_LOGIN) $gen_desc = 'Login ("Remember me")'; |
- | |
69 | if ($gen === self::JWT_GENERATOR_MANUAL) $gen_desc = 'Manually created'; |
- | |
70 | 83 | ||
71 | OIDplus::config()->prepareConfigKey($cfg, 'Revoke timestamp of all JWT tokens for $sub with generator $gen ($gen_desc)', "$bl_time", OIDplusConfig::PROTECTION_HIDDEN, function($value) {}); |
84 | OIDplus::config()->prepareConfigKey($cfg, 'Revoke timestamp of all JWT tokens for $sub with generator $gen ($gen_desc)', "$bl_time", OIDplusConfig::PROTECTION_HIDDEN, function($value) {}); |
72 | OIDplus::config()->setValue($cfg, $bl_time); |
85 | OIDplus::config()->setValue($cfg, $bl_time); |
73 | } |
86 | } |
74 | 87 | ||
Line 82... | Line 95... | ||
82 | $cfg = self::jwtGetBlacklistConfigKey($gen, $sub); |
95 | $cfg = self::jwtGetBlacklistConfigKey($gen, $sub); |
83 | return (int)OIDplus::config()->getValue($cfg,0); |
96 | return (int)OIDplus::config()->getValue($cfg,0); |
84 | } |
97 | } |
85 | 98 | ||
86 | /** |
99 | /** |
- | 100 | * Do various checks if the token is allowed and not blacklisted |
|
87 | * @param OIDplusAuthContentStore $contentProvider |
101 | * @param OIDplusAuthContentStore $contentProvider |
- | 102 | * @param int $validGenerators Bitmask which generators to allow (-1 = allow all) |
|
88 | * @return void |
103 | * @return void |
89 | * @throws OIDplusException |
104 | * @throws OIDplusException |
90 | */ |
105 | */ |
91 | private static function jwtSecurityCheck(OIDplusAuthContentStore $contentProvider) { |
106 | private static function jwtSecurityCheck(OIDplusAuthContentStore $contentProvider, int $validGenerators=-1) { |
92 | // Check if the token is intended for us |
107 | // Check if the token is intended for us |
93 | if ($contentProvider->getValue('aud','') !== OIDplus::getEditionInfo()['jwtaud']) { |
108 | if ($contentProvider->getValue('aud','') !== OIDplus::getEditionInfo()['jwtaud']) { |
94 | throw new OIDplusException(_L('Token has wrong audience')); |
109 | throw new OIDplusException(_L('Token has wrong audience')); |
95 | } |
110 | } |
96 | $gen = $contentProvider->getValue('oidplus_generator', -1); |
111 | $gen = $contentProvider->getValue('oidplus_generator', -1); |
Line 107... | Line 122... | ||
107 | if (($has_ra) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_AJAX_USER', true)) { |
122 | if (($has_ra) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_AJAX_USER', true)) { |
108 | // Generator: plugins/viathinksoft/raPages/910_automated_ajax_calls/OIDplusPageRaAutomatedAJAXCalls.class.php |
123 | // Generator: plugins/viathinksoft/raPages/910_automated_ajax_calls/OIDplusPageRaAutomatedAJAXCalls.class.php |
109 | throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_AJAX_USER')); |
124 | throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_AJAX_USER')); |
110 | } |
125 | } |
111 | } |
126 | } |
- | 127 | else if ($gen === self::JWT_GENERATOR_REST) { |
|
- | 128 | if (($has_admin) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_REST_ADMIN', true)) { |
|
- | 129 | // Generator: plugins/viathinksoft/adminPages/911_rest_api/OIDplusPageAdminRestApi.class.php |
|
- | 130 | throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_REST_ADMIN')); |
|
- | 131 | } |
|
- | 132 | if (($has_ra) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_REST_USER', true)) { |
|
- | 133 | // Generator: plugins/viathinksoft/raPages/911_rest_api/OIDplusPageRaRestApi.class.php |
|
- | 134 | throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_REST_USER')); |
|
- | 135 | } |
|
- | 136 | } |
|
112 | else if ($gen === self::JWT_GENERATOR_LOGIN) { |
137 | else if ($gen === self::JWT_GENERATOR_LOGIN) { |
113 | // Used for feature "Remember me" (use JWT token in a cookie as alternative to PHP session): |
138 | // Used for feature "Remember me" (use JWT token in a cookie as alternative to PHP session): |
114 | // - No PHP session will be used |
139 | // - No PHP session will be used |
115 | // - Session will not be bound to IP address (therefore, you can switch between mobile/WiFi for example) |
140 | // - Session will not be bound to IP address (therefore, you can switch between mobile/WiFi for example) |
116 | // - No server-side session needed |
141 | // - No server-side session needed |
Line 154... | Line 179... | ||
154 | if (isset($_SERVER['REMOTE_ADDR']) && ($ip !== $_SERVER['REMOTE_ADDR'])) { |
179 | if (isset($_SERVER['REMOTE_ADDR']) && ($ip !== $_SERVER['REMOTE_ADDR'])) { |
155 | throw new OIDplusException(_L('Your IP address is not allowed to use this token')); |
180 | throw new OIDplusException(_L('Your IP address is not allowed to use this token')); |
156 | } |
181 | } |
157 | } |
182 | } |
158 | 183 | ||
159 | // Checks which are dependent on the generator |
184 | // Checks if JWT are dependent on the generator |
160 | if ($gen === self::JWT_GENERATOR_LOGIN) { |
185 | if ($validGenerators !== -1) { |
161 | if (!isset($_COOKIE[self::COOKIE_NAME])) { |
- | |
162 | throw new OIDplusException(_L('This kind of JWT token can only be used with the %1 request type','COOKIE')); |
- | |
163 | } |
- | |
164 | } |
- | |
165 | if ($gen === self::JWT_GENERATOR_AJAX) { |
186 | if (($gen & $validGenerators) === 0) { |
166 | if (!isset($_GET[self::COOKIE_NAME]) && !isset($_POST[self::COOKIE_NAME])) { |
- | |
167 | throw new OIDplusException(_L('This kind of JWT token can only be used with the %1 request type','GET/POST')); |
187 | throw new OIDplusException(_L('This kind of JWT token (%1) cannot be used in this request type', self::generatorName($gen))); |
168 | } |
- | |
169 | if (isset($_SERVER['SCRIPT_FILENAME']) && (strtolower(basename($_SERVER['SCRIPT_FILENAME'])) !== 'ajax.php')) { |
- | |
170 | throw new OIDplusException(_L('This kind of JWT token can only be used in ajax.php')); |
- | |
171 | } |
188 | } |
172 | } |
189 | } |
173 | } |
190 | } |
174 | 191 | ||
175 | // Override abstract functions |
192 | // Override abstract functions |
Line 248... | Line 265... | ||
248 | * @return OIDplusAuthContentStore|null |
265 | * @return OIDplusAuthContentStore|null |
249 | * @throws OIDplusException |
266 | * @throws OIDplusException |
250 | */ |
267 | */ |
251 | public static function getActiveProvider()/*: ?OIDplusAuthContentStore*/ { |
268 | public static function getActiveProvider()/*: ?OIDplusAuthContentStore*/ { |
252 | if (!self::$contentProvider) { |
269 | if (!self::$contentProvider) { |
253 | $jwt = ''; |
- | |
254 | if (isset($_COOKIE[self::COOKIE_NAME])) $jwt = $_COOKIE[self::COOKIE_NAME]; |
- | |
255 | if (isset($_POST[self::COOKIE_NAME])) $jwt = $_POST[self::COOKIE_NAME]; |
- | |
256 | if (isset($_GET[self::COOKIE_NAME])) $jwt = $_GET[self::COOKIE_NAME]; |
- | |
257 | 270 | ||
258 | if (!empty($jwt)) { |
271 | $tmp = null; |
259 | $tmp = new OIDplusAuthContentStoreJWT(); |
272 | $silent_error = false; |
260 | 273 | ||
261 | try { |
274 | try { |
262 | // Decode the JWT. In this step, the signature as well as EXP/NBF times will be checked |
- | |
263 | $tmp->loadJWT($jwt); |
- | |
264 | 275 | ||
- | 276 | $rel_url = substr($_SERVER['REQUEST_URI'], strlen(OIDplus::webpath(null, OIDplus::PATH_RELATIVE_TO_ROOT))); |
|
- | 277 | 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. |
|
- | 278 | ||
265 | // Do various checks if the token is allowed and not blacklisted |
279 | // REST may only use Bearer Authentication |
266 | self::jwtSecurityCheck($tmp); |
280 | $bearer = getBearerToken(); |
- | 281 | if (!is_null($bearer)) { |
|
- | 282 | $silent_error = false; |
|
- | 283 | $tmp = new OIDplusAuthContentStoreJWT(); |
|
- | 284 | $tmp->loadJWT($bearer); |
|
- | 285 | self::jwtSecurityCheck($tmp, self::JWT_GENERATOR_REST | self::JWT_GENERATOR_MANUAL); |
|
- | 286 | } |
|
- | 287 | ||
- | 288 | } else { |
|
- | 289 | ||
- | 290 | // A web-visitor (HTML and AJAX, but not REST) can use a JWT "remember me" Cookie |
|
- | 291 | if (isset($_COOKIE[self::COOKIE_NAME])) { |
|
- | 292 | $silent_error = true; |
|
- | 293 | $tmp = new OIDplusAuthContentStoreJWT(); |
|
- | 294 | $tmp->loadJWT($_COOKIE[self::COOKIE_NAME]); |
|
- | 295 | self::jwtSecurityCheck($tmp, self::JWT_GENERATOR_LOGIN | self::JWT_GENERATOR_MANUAL); |
|
- | 296 | } |
|
- | 297 | ||
- | 298 | // AJAX may additionally use GET/POST automated AJAX (in addition to the normal JWT "remember me" Cookie) |
|
- | 299 | if (isset($_SERVER['SCRIPT_FILENAME']) && (strtolower(basename($_SERVER['SCRIPT_FILENAME'])) !== 'ajax.php')) { |
|
- | 300 | if (isset($_POST[self::COOKIE_NAME])) { |
|
- | 301 | $silent_error = false; |
|
- | 302 | $tmp = new OIDplusAuthContentStoreJWT(); |
|
- | 303 | $tmp->loadJWT($_POST[self::COOKIE_NAME]); |
|
- | 304 | self::jwtSecurityCheck($tmp, self::JWT_GENERATOR_AJAX | self::JWT_GENERATOR_MANUAL); |
|
- | 305 | } |
|
- | 306 | if (isset($_GET[self::COOKIE_NAME])) { |
|
- | 307 | $silent_error = false; |
|
- | 308 | $tmp = new OIDplusAuthContentStoreJWT(); |
|
- | 309 | $tmp->loadJWT($_GET[self::COOKIE_NAME]); |
|
- | 310 | self::jwtSecurityCheck($tmp, self::JWT_GENERATOR_AJAX | self::JWT_GENERATOR_MANUAL); |
|
- | 311 | } |
|
- | 312 | } |
|
- | 313 | ||
- | 314 | } |
|
- | 315 | ||
267 | } catch (\Exception $e) { |
316 | } catch (\Exception $e) { |
268 | if (isset($_GET[self::COOKIE_NAME]) || isset($_POST[self::COOKIE_NAME])) { |
317 | if (!$silent_error) { |
269 | // Most likely an AJAX request. We can throw an Exception |
318 | // Most likely an AJAX request. We can throw an Exception |
270 | throw new OIDplusException(_L('The JWT token was rejected: %1',$e->getMessage())); |
319 | throw new OIDplusException(_L('The JWT token was rejected: %1',$e->getMessage())); |
271 | } else { |
320 | } else { |
272 | // Most likely an expired Cookie/Login session. We must not throw an Exception, otherwise we will break jsTree |
321 | // Most likely an expired Cookie/Login session. We must not throw an Exception, otherwise we will break jsTree |
273 | OIDplus::cookieUtils()->unsetcookie(self::COOKIE_NAME); |
322 | OIDplus::cookieUtils()->unsetcookie(self::COOKIE_NAME); |
Line 275... | Line 324... | ||
275 | } |
324 | } |
276 | } |
325 | } |
277 | 326 | ||
278 | self::$contentProvider = $tmp; |
327 | self::$contentProvider = $tmp; |
279 | } |
328 | } |
280 | } |
- | |
281 | 329 | ||
282 | return self::$contentProvider; |
330 | return self::$contentProvider; |
283 | } |
331 | } |
284 | 332 | ||
285 | /** |
333 | /** |
Line 295... | Line 343... | ||
295 | self::$contentProvider = $this; |
343 | self::$contentProvider = $this; |
296 | } else { |
344 | } else { |
297 | $gen = $this->getValue('oidplus_generator',-1); |
345 | $gen = $this->getValue('oidplus_generator',-1); |
298 | switch ($gen) { |
346 | switch ($gen) { |
299 | case OIDplusAuthContentStoreJWT::JWT_GENERATOR_AJAX : |
347 | case OIDplusAuthContentStoreJWT::JWT_GENERATOR_AJAX : |
- | 348 | case OIDplusAuthContentStoreJWT::JWT_GENERATOR_REST : |
|
300 | case OIDplusAuthContentStoreJWT::JWT_GENERATOR_MANUAL : |
349 | case OIDplusAuthContentStoreJWT::JWT_GENERATOR_MANUAL : |
301 | throw new OIDplusException(_L('This kind of JWT token cannot be altered. Therefore you cannot do this action.')); |
350 | throw new OIDplusException(_L('This kind of JWT token cannot be altered. Therefore you cannot do this action.')); |
302 | case OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN : |
351 | case OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN : |
303 | if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_USER', true)) { |
352 | if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_USER', true)) { |
304 | throw new OIDplusException(_L('You cannot add this login credential to your existing "remember me" session. You need to log-out first.')); |
353 | throw new OIDplusException(_L('You cannot add this login credential to your existing "remember me" session. You need to log-out first.')); |
Line 325... | Line 374... | ||
325 | self::$contentProvider = $this; |
374 | self::$contentProvider = $this; |
326 | } else { |
375 | } else { |
327 | $gen = $this->getValue('oidplus_generator',-1); |
376 | $gen = $this->getValue('oidplus_generator',-1); |
328 | switch ($gen) { |
377 | switch ($gen) { |
329 | case OIDplusAuthContentStoreJWT::JWT_GENERATOR_AJAX : |
378 | case OIDplusAuthContentStoreJWT::JWT_GENERATOR_AJAX : |
- | 379 | case OIDplusAuthContentStoreJWT::JWT_GENERATOR_REST : |
|
330 | case OIDplusAuthContentStoreJWT::JWT_GENERATOR_MANUAL : |
380 | case OIDplusAuthContentStoreJWT::JWT_GENERATOR_MANUAL : |
331 | throw new OIDplusException(_L('This kind of JWT token cannot be altered. Therefore you cannot do this action.')); |
381 | throw new OIDplusException(_L('This kind of JWT token cannot be altered. Therefore you cannot do this action.')); |
332 | case OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN : |
382 | case OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN : |
333 | if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) { |
383 | if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) { |
334 | throw new OIDplusException(_L('You cannot add this login credential to your existing "remember me" session. You need to log-out first.')); |
384 | throw new OIDplusException(_L('You cannot add this login credential to your existing "remember me" session. You need to log-out first.')); |
Line 344... | Line 394... | ||
344 | } |
394 | } |
345 | 395 | ||
346 | // Individual functions |
396 | // Individual functions |
347 | 397 | ||
348 | /** |
398 | /** |
- | 399 | * Decode the JWT. In this step, the signature as well as EXP/NBF times will be checked |
|
349 | * @param string $jwt |
400 | * @param string $jwt |
350 | * @return void |
401 | * @return void |
351 | * @throws OIDplusException |
402 | * @throws OIDplusException |
352 | */ |
403 | */ |
353 | public function loadJWT(string $jwt) { |
404 | public function loadJWT(string $jwt) { |