Rev 576 | Rev 591 | Go to most recent revision | Show entire file | Regard whitespace | Details | Blame | Last modification | View Log | RSS feed
Rev 576 | Rev 585 | ||
---|---|---|---|
Line 19... | Line 19... | ||
19 | 19 | ||
20 | if (!defined('INSIDE_OIDPLUS')) die(); |
20 | if (!defined('INSIDE_OIDPLUS')) die(); |
21 | 21 | ||
22 | class OIDplusAuthContentStoreJWT extends OIDplusAuthContentStoreDummy { |
22 | class OIDplusAuthContentStoreJWT extends OIDplusAuthContentStoreDummy { |
23 | 23 | ||
- | 24 | const COOKIE_NAME = 'OIDPLUS_AUTH_JWT'; |
|
- | 25 | ||
- | 26 | const JWT_GENERATOR_AJAX = 0; // "Automated AJAX" plugin |
|
- | 27 | const JWT_GENERATOR_LOGIN = 1; // "Remember me" login method |
|
- | 28 | const JWT_GENERATOR_MANUAL = 2; // "Manually crafted" JWT tokens |
|
- | 29 | ||
- | 30 | private static function jwtGetBlacklistConfigKey($gen, $sub) { |
|
- | 31 | // Note: Needs to be <= 50 characters! |
|
- | 32 | return 'jwt_blacklist_gen('.$gen.')_sub('.trim(base64_encode(md5($sub,true)),'=').')'; |
|
- | 33 | } |
|
- | 34 | ||
- | 35 | public static function jwtBlacklist($gen, $sub) { |
|
- | 36 | $cfg = self::jwtGetBlacklistConfigKey($gen, $sub); |
|
- | 37 | $bl_time = time()-1; |
|
- | 38 | ||
- | 39 | $gen_desc = 'Unknown'; |
|
- | 40 | if ($gen === self::JWT_GENERATOR_AJAX) $gen_desc = 'Automated AJAX calls'; |
|
- | 41 | if ($gen === self::JWT_GENERATOR_LOGIN) $gen_desc = 'Login ("Remember me")'; |
|
- | 42 | if ($gen === self::JWT_GENERATOR_MANUAL) $gen_desc = 'Manually created'; |
|
- | 43 | ||
- | 44 | OIDplus::config()->prepareConfigKey($cfg, 'Revoke timestamp of all JWT tokens for $sub with generator $gen ($gen_desc)', $bl_time, OIDplusConfig::PROTECTION_HIDDEN, function($value) {}); |
|
- | 45 | OIDplus::config()->setValue($cfg, $bl_time); |
|
- | 46 | } |
|
- | 47 | ||
- | 48 | public static function jwtGetBlacklistTime($gen, $sub) { |
|
- | 49 | $cfg = self::jwtGetBlacklistConfigKey($gen, $sub); |
|
- | 50 | return OIDplus::config()->getValue($cfg,0); |
|
- | 51 | } |
|
- | 52 | ||
- | 53 | private static function jwtSecurityCheck($contentProvider) { |
|
- | 54 | // Check if the token is intended for us |
|
- | 55 | if ($contentProvider->getValue('aud','') !== "http://oidplus.com") { |
|
- | 56 | throw new OIDplusException(_L('Token has wrong audience')); |
|
- | 57 | } |
|
- | 58 | $gen = $contentProvider->getValue('oidplus_generator', -1); |
|
- | 59 | ||
- | 60 | $has_admin = $contentProvider->isAdminLoggedIn(); |
|
- | 61 | $has_ra = $contentProvider->raNumLoggedIn() > 0; |
|
- | 62 | ||
- | 63 | // Check if the token generator is allowed |
|
- | 64 | if ($gen === self::JWT_GENERATOR_AJAX) { |
|
- | 65 | if (($has_admin) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_AJAX_ADMIN', true)) { |
|
- | 66 | // Generator: plugins/adminPages/910_automated_ajax_calls/OIDplusPageAdminAutomatedAJAXCalls.class.php |
|
- | 67 | throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_AJAX_ADMIN')); |
|
- | 68 | } |
|
- | 69 | if (($has_ra) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_AJAX_USER', true)) { |
|
- | 70 | // Generator: plugins/raPages/910_automated_ajax_calls/OIDplusPageRaAutomatedAJAXCalls.class.php |
|
- | 71 | throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_AJAX_USER')); |
|
- | 72 | } |
|
- | 73 | } |
|
- | 74 | else if ($gen === self::JWT_GENERATOR_LOGIN) { |
|
- | 75 | // Used for feature "Remember me" (use JWT token in a cookie as alternative to PHP session): |
|
- | 76 | // - No PHP session will be used |
|
- | 77 | // - Session will not be bound to IP address (therefore, you can switch between mobile/WiFi for example) |
|
- | 78 | // - No server-side session needed |
|
- | 79 | if (($has_admin) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) { |
|
- | 80 | throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_ADMIN')); |
|
- | 81 | } |
|
- | 82 | if (($has_ra) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_USER', true)) { |
|
- | 83 | throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_USER')); |
|
- | 84 | } |
|
- | 85 | } |
|
- | 86 | else if ($gen === self::JWT_GENERATOR_MANUAL) { |
|
- | 87 | // Generator 2 are "hand-crafted" tokens |
|
- | 88 | if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_MANUAL', false)) { |
|
- | 89 | throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_MANUAL')); |
|
- | 90 | } |
|
- | 91 | } else { |
|
- | 92 | throw new OIDplusException(_L('Token generator %1 not recognized',$gen)); |
|
- | 93 | } |
|
- | 94 | ||
- | 95 | // Make sure that the IAT (issued at time) isn't in a blacklisted timeframe |
|
- | 96 | // When an user believes that a token was compromised, then they can blacklist the tokens identified by their "iat" ("Issued at") property |
|
- | 97 | // When a user logs out of a "remember me" session, the JWT token will be blacklisted as well |
|
- | 98 | // Small side effect: All "remember me" sessions of that user will be revoked then |
|
- | 99 | $sublist = $contentProvider->loggedInRaList(); |
|
- | 100 | foreach ($sublist as &$sub) { |
|
- | 101 | $sub = $sub->raEmail(); |
|
- | 102 | } |
|
- | 103 | if ($has_admin) $sublist[] = 'admin'; |
|
- | 104 | foreach ($sublist as $sub) { |
|
- | 105 | $bl_time = self::jwtGetBlacklistTime($gen, $sub); |
|
- | 106 | $iat = $contentProvider->getValue('iat',0); |
|
- | 107 | if ($iat <= $bl_time) { |
|
- | 108 | 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))); |
|
- | 109 | } |
|
- | 110 | } |
|
- | 111 | ||
- | 112 | // Optional feature: Limit the JWT to a specific IP address |
|
- | 113 | // Currently not used in OIDplus |
|
- | 114 | $ip = $contentProvider->getValue('ip',''); |
|
- | 115 | if ($ip !== '') { |
|
- | 116 | if (isset($_SERVER['REMOTE_ADDR']) && ($ip !== $_SERVER['REMOTE_ADDR'])) { |
|
- | 117 | throw new OIDplusException(_L('Your IP address is not allowed to use this token')); |
|
- | 118 | } |
|
- | 119 | } |
|
- | 120 | ||
- | 121 | // Checks which are dependent on the generator |
|
- | 122 | if ($gen === self::JWT_GENERATOR_LOGIN) { |
|
- | 123 | if (!isset($_COOKIE[self::COOKIE_NAME])) { |
|
- | 124 | throw new OIDplusException(_L('This kind of JWT token can only be used with the %1 request type','COOKIE')); |
|
- | 125 | } |
|
- | 126 | } |
|
- | 127 | if ($gen === self::JWT_GENERATOR_AJAX) { |
|
- | 128 | if (!isset($_GET[self::COOKIE_NAME]) && !isset($_POST[self::COOKIE_NAME])) { |
|
- | 129 | throw new OIDplusException(_L('This kind of JWT token can only be used with the %1 request type','GET/POST')); |
|
- | 130 | } |
|
- | 131 | if (isset($_SERVER['SCRIPT_FILENAME']) && (strtolower(basename($_SERVER['SCRIPT_FILENAME'])) !== 'ajax.php')) { |
|
- | 132 | throw new OIDplusException(_L('This kind of JWT token can only be used in ajax.php')); |
|
- | 133 | } |
|
- | 134 | } |
|
- | 135 | } |
|
- | 136 | ||
- | 137 | // Override abstract functions |
|
- | 138 | ||
- | 139 | public function activate() { |
|
- | 140 | $token = $this->getJWTToken(); |
|
- | 141 | $exp = $this->getValue('exp',0); |
|
- | 142 | OIDplus::cookieUtils()->setcookie(self::COOKIE_NAME, $token, $exp, false); |
|
- | 143 | } |
|
- | 144 | ||
- | 145 | public function destroySession() { |
|
- | 146 | OIDplus::cookieUtils()->unsetcookie(self::COOKIE_NAME); |
|
- | 147 | } |
|
- | 148 | ||
- | 149 | public function raLogout($email) { |
|
- | 150 | $gen = $this->getValue('oidplus_generator', -1); |
|
- | 151 | if ($gen >= 0) self::jwtBlacklist($gen, $email); |
|
- | 152 | parent::raLogout($email); |
|
- | 153 | } |
|
- | 154 | ||
- | 155 | public function raLogoutEx($email, &$loginfo) { |
|
- | 156 | $this->raLogout($email); |
|
- | 157 | $loginfo = 'from JWT session'; |
|
- | 158 | } |
|
- | 159 | ||
- | 160 | public function adminLogout() { |
|
- | 161 | $gen = $this->getValue('oidplus_generator', -1); |
|
- | 162 | if ($gen >= 0) self::jwtBlacklist($gen, 'admin'); |
|
- | 163 | parent::adminLogout(); |
|
- | 164 | } |
|
- | 165 | ||
- | 166 | public function adminLogoutEx(&$loginfo) { |
|
- | 167 | $this->adminLogout(); |
|
- | 168 | $loginfo = 'from JWT session'; |
|
- | 169 | } |
|
- | 170 | ||
- | 171 | public static function getActiveProvider() { |
|
- | 172 | static $contentProvider = null; |
|
- | 173 | ||
- | 174 | if (!$contentProvider) { |
|
- | 175 | $jwt = ''; |
|
- | 176 | if (isset($_COOKIE[self::COOKIE_NAME])) $jwt = $_COOKIE[self::COOKIE_NAME]; |
|
- | 177 | if (isset($_POST[self::COOKIE_NAME])) $jwt = $_POST[self::COOKIE_NAME]; |
|
- | 178 | if (isset($_GET[self::COOKIE_NAME])) $jwt = $_GET[self::COOKIE_NAME]; |
|
- | 179 | ||
- | 180 | if (!empty($jwt)) { |
|
- | 181 | $tmp = new OIDplusAuthContentStoreJWT(); |
|
- | 182 | ||
- | 183 | try { |
|
- | 184 | // Decode the JWT. In this step, the signature as well as EXP/NBF times will be checked |
|
- | 185 | $tmp->loadJWT($jwt); |
|
- | 186 | ||
- | 187 | // Do various checks if the token is allowed and not blacklisted |
|
- | 188 | self::jwtSecurityCheck($tmp); |
|
- | 189 | } catch (Exception $e) { |
|
- | 190 | if (isset($_GET[self::COOKIE_NAME]) || isset($_POST[self::COOKIE_NAME])) { |
|
- | 191 | // Most likely an AJAX request. We can throw an Exception |
|
- | 192 | throw new OIDplusException(_L('The JWT token was rejected: %1',$e->getMessage())); |
|
- | 193 | } else { |
|
- | 194 | // Most likely an expired Cookie/Login session. We must not throw an Exception, otherwise we will break jsTree |
|
- | 195 | OIDplus::cookieUtils()->unsetcookie(self::COOKIE_NAME); |
|
- | 196 | return null; |
|
- | 197 | } |
|
- | 198 | } |
|
- | 199 | ||
- | 200 | $contentProvider = $tmp; |
|
- | 201 | } |
|
- | 202 | } |
|
- | 203 | ||
- | 204 | return $contentProvider; |
|
- | 205 | } |
|
- | 206 | ||
- | 207 | public function raLoginEx($email, &$loginfo) { |
|
- | 208 | if (is_null(self::getActiveProvider())) { |
|
- | 209 | $this->raLogin($email); |
|
- | 210 | $loginfo = 'into new JWT session'; |
|
- | 211 | } else { |
|
- | 212 | $gen = $this->getValue('oidplus_generator',-1); |
|
- | 213 | switch ($gen) { |
|
- | 214 | case OIDplusAuthContentStoreJWT::JWT_GENERATOR_AJAX : |
|
- | 215 | case OIDplusAuthContentStoreJWT::JWT_GENERATOR_MANUAL : |
|
- | 216 | throw new OIDplusException(_L('This kind of JWT token cannot be altered. Therefore you cannot do this action.')); |
|
- | 217 | break; |
|
- | 218 | case OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN : |
|
- | 219 | if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_USER', true)) { |
|
- | 220 | throw new OIDplusException(_L('You cannot add this login credential to your existing "remember me" session. You need to log-out first.')); |
|
- | 221 | } |
|
- | 222 | break; |
|
- | 223 | default: |
|
- | 224 | assert(false); // This cannot happen because jwtSecurityCheck will check for unknown generators |
|
- | 225 | break; |
|
- | 226 | } |
|
- | 227 | $this->raLogin($email); |
|
- | 228 | $loginfo = 'into existing JWT session'; |
|
- | 229 | } |
|
- | 230 | } |
|
- | 231 | ||
- | 232 | public function adminLoginEx(&$loginfo) { |
|
- | 233 | if (is_null(self::getActiveProvider())) { |
|
- | 234 | $this->adminLogin(); |
|
- | 235 | $loginfo = 'into new JWT session'; |
|
- | 236 | } else { |
|
- | 237 | $gen = $this->getValue('oidplus_generator',-1); |
|
- | 238 | switch ($gen) { |
|
- | 239 | case OIDplusAuthContentStoreJWT::JWT_GENERATOR_AJAX : |
|
- | 240 | case OIDplusAuthContentStoreJWT::JWT_GENERATOR_MANUAL : |
|
- | 241 | throw new OIDplusException(_L('This kind of JWT token cannot be altered. Therefore you cannot do this action.')); |
|
- | 242 | break; |
|
- | 243 | case OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN : |
|
- | 244 | if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) { |
|
- | 245 | throw new OIDplusException(_L('You cannot add this login credential to your existing "remember me" session. You need to log-out first.')); |
|
- | 246 | } |
|
- | 247 | break; |
|
- | 248 | default: |
|
- | 249 | assert(false); // This cannot happen because jwtSecurityCheck will check for unknown generators |
|
- | 250 | break; |
|
- | 251 | } |
|
- | 252 | $this->adminLogin(); |
|
- | 253 | $loginfo = 'into existing JWT session'; |
|
- | 254 | } |
|
- | 255 | } |
|
- | 256 | ||
24 | // Individual functions |
257 | // Individual functions |
25 | 258 | ||
26 | public function loadJWT($jwt) { |
259 | public function loadJWT($jwt) { |
27 | \Firebase\JWT\JWT::$leeway = 60; // leeway in seconds |
260 | \Firebase\JWT\JWT::$leeway = 60; // leeway in seconds |
28 | if (OIDplus::getPkiStatus()) { |
261 | if (OIDplus::getPkiStatus()) { |
Line 33... | Line 266... | ||
33 | $key = hash_pbkdf2('sha512', $key, '', 10000, 64/*256bit*/, false); |
266 | $key = hash_pbkdf2('sha512', $key, '', 10000, 64/*256bit*/, false); |
34 | $this->content = (array) \Firebase\JWT\JWT::decode($jwt, $key, array('HS256', 'HS384', 'HS512')); |
267 | $this->content = (array) \Firebase\JWT\JWT::decode($jwt, $key, array('HS256', 'HS384', 'HS512')); |
35 | } |
268 | } |
36 | } |
269 | } |
37 | 270 | ||
38 | public function getJWTToken($lifetime=null) { |
271 | public function getJWTToken() { |
39 | $payload = $this->content; |
272 | $payload = $this->content; |
40 | $payload["iss"] = "http://oidplus.com"; |
273 | $payload["iss"] = "http://oidplus.com"; |
41 | $payload["aud"] = "http://oidplus.com"; |
274 | $payload["aud"] = "http://oidplus.com"; |
42 | $payload["jti"] = gen_uuid(); |
275 | $payload["jti"] = gen_uuid(); |
43 | $payload["iat"] = time(); |
276 | $payload["iat"] = time(); |
44 | if (!is_null($lifetime)) $payload["exp"] = time() + $lifetime; |
- | |
45 | 277 | ||
46 | if (OIDplus::getPkiStatus()) { |
278 | if (OIDplus::getPkiStatus()) { |
47 | $privKey = OIDplus::config()->getValue('oidplus_private_key'); |
279 | $privKey = OIDplus::config()->getValue('oidplus_private_key'); |
48 | return \Firebase\JWT\JWT::encode($payload, $privKey, 'RS512'); |
280 | return \Firebase\JWT\JWT::encode($payload, $privKey, 'RS512'); |
49 | } else { |
281 | } else { |