33,15 → 33,19 |
/** |
* "Automated AJAX" plugin |
*/ |
const JWT_GENERATOR_AJAX = 0; |
const JWT_GENERATOR_AJAX = 10; |
/** |
* "REST API" plugin |
*/ |
const JWT_GENERATOR_REST = 20; |
/** |
* "Remember me" login method |
*/ |
const JWT_GENERATOR_LOGIN = 1; |
const JWT_GENERATOR_LOGIN = 40; |
/** |
* "Manually crafted" JWT tokens |
*/ |
const JWT_GENERATOR_MANUAL = 2; |
const JWT_GENERATOR_MANUAL = 80; |
|
/** |
* @param int $gen OIDplusAuthContentStoreJWT::JWT_GENERATOR_... |
49,11 → 53,23 |
* @return string |
*/ |
private static function jwtGetBlacklistConfigKey(int $gen, string $sub): string { |
// Note: Needs to be <= 50 characters! |
// Note: Needs to be <= 50 characters! If $gen is 2 chars, then the config key is 49 chars long |
return 'jwt_blacklist_gen('.$gen.')_sub('.trim(base64_encode(md5($sub,true)),'=').')'; |
} |
|
/** |
* @param int $gen |
*/ |
private static function generatorName($gen) { |
// Note: The strings are not translated, because the name is used in config keys or logs |
if ($gen === self::JWT_GENERATOR_AJAX) return 'Automated AJAX calls'; |
if ($gen === self::JWT_GENERATOR_REST) return 'REST API'; |
if ($gen === self::JWT_GENERATOR_LOGIN) return 'Login ("Remember me")'; |
if ($gen === self::JWT_GENERATOR_MANUAL) return 'Manually created'; |
return 'Unknown generator'; |
} |
|
/** |
* @param int $gen OIDplusAuthContentStoreJWT::JWT_GENERATOR_... |
* @param string $sub |
* @return void |
63,10 → 79,7 |
$cfg = self::jwtGetBlacklistConfigKey($gen, $sub); |
$bl_time = time()-1; |
|
$gen_desc = 'Unknown'; |
if ($gen === self::JWT_GENERATOR_AJAX) $gen_desc = 'Automated AJAX calls'; |
if ($gen === self::JWT_GENERATOR_LOGIN) $gen_desc = 'Login ("Remember me")'; |
if ($gen === self::JWT_GENERATOR_MANUAL) $gen_desc = 'Manually created'; |
$gen_desc = self::generatorName($gen); |
|
OIDplus::config()->prepareConfigKey($cfg, 'Revoke timestamp of all JWT tokens for $sub with generator $gen ($gen_desc)', "$bl_time", OIDplusConfig::PROTECTION_HIDDEN, function($value) {}); |
OIDplus::config()->setValue($cfg, $bl_time); |
84,11 → 97,13 |
} |
|
/** |
* Do various checks if the token is allowed and not blacklisted |
* @param OIDplusAuthContentStore $contentProvider |
* @param int $validGenerators Bitmask which generators to allow (-1 = allow all) |
* @return void |
* @throws OIDplusException |
*/ |
private static function jwtSecurityCheck(OIDplusAuthContentStore $contentProvider) { |
private static function jwtSecurityCheck(OIDplusAuthContentStore $contentProvider, int $validGenerators=-1) { |
// Check if the token is intended for us |
if ($contentProvider->getValue('aud','') !== OIDplus::getEditionInfo()['jwtaud']) { |
throw new OIDplusException(_L('Token has wrong audience')); |
109,6 → 124,16 |
throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_AJAX_USER')); |
} |
} |
else if ($gen === self::JWT_GENERATOR_REST) { |
if (($has_admin) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_REST_ADMIN', true)) { |
// Generator: plugins/viathinksoft/adminPages/911_rest_api/OIDplusPageAdminRestApi.class.php |
throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_REST_ADMIN')); |
} |
if (($has_ra) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_REST_USER', true)) { |
// Generator: plugins/viathinksoft/raPages/911_rest_api/OIDplusPageRaRestApi.class.php |
throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_REST_USER')); |
} |
} |
else if ($gen === self::JWT_GENERATOR_LOGIN) { |
// Used for feature "Remember me" (use JWT token in a cookie as alternative to PHP session): |
// - No PHP session will be used |
156,21 → 181,13 |
} |
} |
|
// Checks which are dependent on the generator |
if ($gen === self::JWT_GENERATOR_LOGIN) { |
if (!isset($_COOKIE[self::COOKIE_NAME])) { |
throw new OIDplusException(_L('This kind of JWT token can only be used with the %1 request type','COOKIE')); |
// Checks if JWT are dependent on the generator |
if ($validGenerators !== -1) { |
if (($gen & $validGenerators) === 0) { |
throw new OIDplusException(_L('This kind of JWT token (%1) cannot be used in this request type', self::generatorName($gen))); |
} |
} |
if ($gen === self::JWT_GENERATOR_AJAX) { |
if (!isset($_GET[self::COOKIE_NAME]) && !isset($_POST[self::COOKIE_NAME])) { |
throw new OIDplusException(_L('This kind of JWT token can only be used with the %1 request type','GET/POST')); |
} |
if (isset($_SERVER['SCRIPT_FILENAME']) && (strtolower(basename($_SERVER['SCRIPT_FILENAME'])) !== 'ajax.php')) { |
throw new OIDplusException(_L('This kind of JWT token can only be used in ajax.php')); |
} |
} |
} |
|
// Override abstract functions |
|
250,22 → 267,54 |
*/ |
public static function getActiveProvider()/*: ?OIDplusAuthContentStore*/ { |
if (!self::$contentProvider) { |
$jwt = ''; |
if (isset($_COOKIE[self::COOKIE_NAME])) $jwt = $_COOKIE[self::COOKIE_NAME]; |
if (isset($_POST[self::COOKIE_NAME])) $jwt = $_POST[self::COOKIE_NAME]; |
if (isset($_GET[self::COOKIE_NAME])) $jwt = $_GET[self::COOKIE_NAME]; |
|
if (!empty($jwt)) { |
$tmp = new OIDplusAuthContentStoreJWT(); |
$tmp = null; |
$silent_error = false; |
|
try { |
// Decode the JWT. In this step, the signature as well as EXP/NBF times will be checked |
$tmp->loadJWT($jwt); |
|
// Do various checks if the token is allowed and not blacklisted |
self::jwtSecurityCheck($tmp); |
$rel_url = substr($_SERVER['REQUEST_URI'], strlen(OIDplus::webpath(null, OIDplus::PATH_RELATIVE_TO_ROOT))); |
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. |
|
// REST may only use Bearer Authentication |
$bearer = getBearerToken(); |
if (!is_null($bearer)) { |
$silent_error = false; |
$tmp = new OIDplusAuthContentStoreJWT(); |
$tmp->loadJWT($bearer); |
self::jwtSecurityCheck($tmp, self::JWT_GENERATOR_REST | self::JWT_GENERATOR_MANUAL); |
} |
|
} else { |
|
// A web-visitor (HTML and AJAX, but not REST) can use a JWT "remember me" Cookie |
if (isset($_COOKIE[self::COOKIE_NAME])) { |
$silent_error = true; |
$tmp = new OIDplusAuthContentStoreJWT(); |
$tmp->loadJWT($_COOKIE[self::COOKIE_NAME]); |
self::jwtSecurityCheck($tmp, self::JWT_GENERATOR_LOGIN | self::JWT_GENERATOR_MANUAL); |
} |
|
// AJAX may additionally use GET/POST automated AJAX (in addition to the normal JWT "remember me" Cookie) |
if (isset($_SERVER['SCRIPT_FILENAME']) && (strtolower(basename($_SERVER['SCRIPT_FILENAME'])) !== 'ajax.php')) { |
if (isset($_POST[self::COOKIE_NAME])) { |
$silent_error = false; |
$tmp = new OIDplusAuthContentStoreJWT(); |
$tmp->loadJWT($_POST[self::COOKIE_NAME]); |
self::jwtSecurityCheck($tmp, self::JWT_GENERATOR_AJAX | self::JWT_GENERATOR_MANUAL); |
} |
if (isset($_GET[self::COOKIE_NAME])) { |
$silent_error = false; |
$tmp = new OIDplusAuthContentStoreJWT(); |
$tmp->loadJWT($_GET[self::COOKIE_NAME]); |
self::jwtSecurityCheck($tmp, self::JWT_GENERATOR_AJAX | self::JWT_GENERATOR_MANUAL); |
} |
} |
|
} |
|
} catch (\Exception $e) { |
if (isset($_GET[self::COOKIE_NAME]) || isset($_POST[self::COOKIE_NAME])) { |
if (!$silent_error) { |
// Most likely an AJAX request. We can throw an Exception |
throw new OIDplusException(_L('The JWT token was rejected: %1',$e->getMessage())); |
} else { |
277,7 → 326,6 |
|
self::$contentProvider = $tmp; |
} |
} |
|
return self::$contentProvider; |
} |
297,6 → 345,7 |
$gen = $this->getValue('oidplus_generator',-1); |
switch ($gen) { |
case OIDplusAuthContentStoreJWT::JWT_GENERATOR_AJAX : |
case OIDplusAuthContentStoreJWT::JWT_GENERATOR_REST : |
case OIDplusAuthContentStoreJWT::JWT_GENERATOR_MANUAL : |
throw new OIDplusException(_L('This kind of JWT token cannot be altered. Therefore you cannot do this action.')); |
case OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN : |
327,6 → 376,7 |
$gen = $this->getValue('oidplus_generator',-1); |
switch ($gen) { |
case OIDplusAuthContentStoreJWT::JWT_GENERATOR_AJAX : |
case OIDplusAuthContentStoreJWT::JWT_GENERATOR_REST : |
case OIDplusAuthContentStoreJWT::JWT_GENERATOR_MANUAL : |
throw new OIDplusException(_L('This kind of JWT token cannot be altered. Therefore you cannot do this action.')); |
case OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN : |
346,6 → 396,7 |
// Individual functions |
|
/** |
* Decode the JWT. In this step, the signature as well as EXP/NBF times will be checked |
* @param string $jwt |
* @return void |
* @throws OIDplusException |