Rev 583 | Rev 590 | Go to most recent revision | Show entire file | Regard whitespace | Details | Blame | Last modification | View Log | RSS feed
Rev 583 | Rev 585 | ||
---|---|---|---|
Line 46... | Line 46... | ||
46 | } |
46 | } |
47 | $a = substr($a, 0, $len*2); |
47 | $a = substr($a, 0, $len*2); |
48 | return hex2bin($a); |
48 | return hex2bin($a); |
49 | } |
49 | } |
50 | 50 | ||
51 | // JWT handling |
51 | // Content provider |
52 | - | ||
53 | const JWT_GENERATOR_AJAX = 0; |
- | |
54 | const JWT_GENERATOR_LOGIN = 1; |
- | |
55 | const JWT_GENERATOR_MANUAL = 2; |
- | |
56 | - | ||
57 | private function jwtGetBlacklistConfigKey($gen, $sub) { |
- | |
58 | // Note: Needs to be <= 50 characters! |
- | |
59 | return 'jwt_blacklist_gen('.$gen.')_sub('.trim(base64_encode(md5($sub,true)),'=').')'; |
- | |
60 | } |
- | |
61 | - | ||
62 | public function jwtBlacklist($gen, $sub) { |
- | |
63 | $cfg = $this->jwtGetBlacklistConfigKey($gen, $sub); |
- | |
64 | $bl_time = time()-1; |
- | |
65 | - | ||
66 | $gen_desc = 'Unknown'; |
- | |
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 | - | ||
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) {}); |
- | |
72 | OIDplus::config()->setValue($cfg, $bl_time); |
- | |
73 | } |
- | |
74 | - | ||
75 | public function jwtGetBlacklistTime($gen, $sub) { |
- | |
76 | $cfg = $this->jwtGetBlacklistConfigKey($gen, $sub); |
- | |
77 | return OIDplus::config()->getValue($cfg,0); |
- | |
78 | } |
- | |
79 | - | ||
80 | protected function jwtSecurityCheck($contentProvider) { |
- | |
81 | // Check if the token is intended for us |
- | |
82 | if ($contentProvider->getValue('aud','') !== "http://oidplus.com") { |
- | |
83 | throw new OIDplusException(_L('Token has wrong audience')); |
- | |
84 | } |
- | |
85 | $gen = $contentProvider->getValue('oidplus_generator', -1); |
- | |
86 | $sub = $contentProvider->getValue('sub', ''); |
- | |
87 | - | ||
88 | // Check if the token generator is allowed |
- | |
89 | if ($gen === self::JWT_GENERATOR_AJAX) { |
- | |
90 | if (($sub === 'admin') && !OIDplus::baseConfig()->getValue('JWT_ALLOW_AJAX_ADMIN', true)) { |
- | |
91 | // Generator: plugins/adminPages/910_automated_ajax_calls/OIDplusPageAdminAutomatedAJAXCalls.class.php |
- | |
92 | throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_AJAX_ADMIN')); |
- | |
93 | } |
- | |
94 | else if (($sub !== 'admin') && !OIDplus::baseConfig()->getValue('JWT_ALLOW_AJAX_USER', true)) { |
- | |
95 | // Generator: plugins/raPages/910_automated_ajax_calls/OIDplusPageRaAutomatedAJAXCalls.class.php |
- | |
96 | throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_AJAX_USER')); |
- | |
97 | } |
- | |
98 | } |
- | |
99 | else if ($gen === self::JWT_GENERATOR_LOGIN) { |
- | |
100 | // Used for feature "Remember me" (use JWT token in a cookie as alternative to PHP session): |
- | |
101 | // - No PHP session will be used |
- | |
102 | // - Session will not be bound to IP address (therefore, you can switch between mobile/WiFi for example) |
- | |
103 | // - No server-side session needed |
- | |
104 | if (($sub === 'admin') && !OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) { |
- | |
105 | throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_ADMIN')); |
- | |
106 | } |
- | |
107 | else if (($sub !== 'admin') && !OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_USER', true)) { |
- | |
108 | throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_USER')); |
- | |
109 | } |
- | |
110 | } |
- | |
111 | else if ($gen === self::JWT_GENERATOR_MANUAL) { |
- | |
112 | // Generator 2 are "hand-crafted" tokens |
- | |
113 | if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_MANUAL', true)) { |
- | |
114 | throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_MANUAL')); |
- | |
115 | } |
- | |
116 | } else { |
- | |
117 | throw new OIDplusException(_L('Token generator %1 not recognized',$gen)); |
- | |
118 | } |
- | |
119 | - | ||
120 | // Make sure that the IAT (issued at time) isn't in a blacklisted timeframe |
- | |
121 | // When an user believes that a token was compromised, then they can blacklist the tokens identified by their "iat" ("Issued at") property |
- | |
122 | $bl_time = $this->jwtGetBlacklistTime($gen, $sub); |
- | |
123 | $iat = $contentProvider->getValue('iat',0); |
- | |
124 | if ($iat <= $bl_time) { |
- | |
125 | 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))); |
- | |
126 | } |
- | |
127 | - | ||
128 | // Optional feature: Limit the JWT to a specific IP address |
- | |
129 | // This could become handy if JWTs are used instead of Login sessions, |
- | |
130 | // and you want to avoid session/JWT hijacking |
- | |
131 | $ip = $contentProvider->getValue('ip',''); |
- | |
132 | if ($ip !== '') { |
- | |
133 | if (isset($_SERVER['REMOTE_ADDR']) && ($ip !== $_SERVER['REMOTE_ADDR'])) { |
- | |
134 | throw new OIDplusException(_L('Your IP address is not allowed to use this token')); |
- | |
135 | } |
- | |
136 | } |
- | |
137 | 52 | ||
138 | // Checks which are dependent on the generator |
53 | public function getAuthMethod() { |
139 | if ($gen === self::JWT_GENERATOR_LOGIN) { |
54 | $acs = $this->getAuthContentStore(); |
140 | if (!isset($_COOKIE['OIDPLUS_AUTH_JWT'])) { |
55 | if (is_null($acs)) return 'null'; |
141 | throw new OIDplusException(_L('This kind of JWT token can only be used with the %1 request type','COOKIE')); |
- | |
142 | } |
- | |
143 | } |
- | |
144 | if ($gen === self::JWT_GENERATOR_AJAX) { |
56 | return get_class($acs); |
145 | if (!isset($_GET['OIDPLUS_AUTH_JWT']) && !isset($_POST['OIDPLUS_AUTH_JWT'])) { |
- | |
146 | throw new OIDplusException(_L('This kind of JWT token can only be used with the %1 request type','GET/POST')); |
- | |
147 | } |
- | |
148 | if (isset($_SERVER['SCRIPT_FILENAME']) && (strtolower(basename($_SERVER['SCRIPT_FILENAME'])) !== 'ajax.php')) { |
- | |
149 | throw new OIDplusException(_L('This kind of JWT token can only be used in ajax.php')); |
- | |
150 | } |
- | |
151 | } |
57 | } |
152 | } |
- | |
153 | - | ||
154 | // Content provider |
- | |
155 | 58 | ||
156 | protected function getAuthContentStore() { |
59 | protected function getAuthContentStore() { |
- | 60 | // Logged in via JWT |
|
- | 61 | $tmp = OIDplusAuthContentStoreJWT::getActiveProvider(); |
|
157 | static $contentProvider = null; |
62 | if ($tmp) return $tmp; |
158 | 63 | ||
159 | if (is_null($contentProvider)) { |
- | |
160 | $jwt = ''; |
- | |
161 | if (isset($_COOKIE['OIDPLUS_AUTH_JWT'])) $jwt = $_COOKIE['OIDPLUS_AUTH_JWT']; |
- | |
162 | if (isset($_POST['OIDPLUS_AUTH_JWT'])) $jwt = $_POST['OIDPLUS_AUTH_JWT']; |
- | |
163 | if (isset($_GET['OIDPLUS_AUTH_JWT'])) $jwt = $_GET['OIDPLUS_AUTH_JWT']; |
- | |
164 | - | ||
165 | if (!empty($jwt)) { |
- | |
166 | $contentProvider = new OIDplusAuthContentStoreJWT(); |
- | |
167 | - | ||
168 | try { |
- | |
169 | // Decode the JWT. In this step, the signature as well as EXP/NBF times will be checked |
- | |
170 | $contentProvider->loadJWT($jwt); |
- | |
171 | - | ||
172 | // Do various checks if the token is allowed and not blacklisted |
- | |
173 | $this->jwtSecurityCheck($contentProvider); |
- | |
174 | } catch (Exception $e) { |
- | |
175 | if (isset($_GET['OIDPLUS_AUTH_JWT']) || isset($_POST['OIDPLUS_AUTH_JWT'])) { |
- | |
176 | // Most likely an AJAX request. We can throw an Exception |
- | |
177 | $contentProvider = null; |
- | |
178 | throw new OIDplusException(_L('The JWT token was rejected: %1',$e->getMessage())); |
- | |
179 | } else { |
- | |
180 | // Most likely an expired Cookie/Login session. We must not throw an Exception, otherwise we will break jsTree |
- | |
181 | $contentProvider = new OIDplusAuthContentStoreSession(); |
- | |
182 | OIDplus::cookieUtils()->unsetcookie('OIDPLUS_AUTH_JWT'); |
- | |
183 | } |
- | |
184 | } |
- | |
185 | } else { |
- | |
186 | // Normal login via web-browser |
64 | // Normal login via web-browser |
- | 65 | // Cookie will only be created once content is stored |
|
187 | $contentProvider = new OIDplusAuthContentStoreSession(); |
66 | $tmp = OIDplusAuthContentStoreSession::getActiveProvider(); |
- | 67 | if ($tmp) return $tmp; |
|
188 | } |
68 | |
- | 69 | // No active session and no JWT token available. User is not logged in. |
|
- | 70 | return null; |
|
189 | } |
71 | } |
190 | 72 | ||
- | 73 | public function getExtendedAttribute($name, $default=NULL) { |
|
- | 74 | $acs = $this->getAuthContentStore(); |
|
- | 75 | if (is_null($acs)) return $default; |
|
191 | return $contentProvider; |
76 | return $acs->getValue($name, $default); |
192 | } |
77 | } |
193 | 78 | ||
194 | // RA authentication functions |
79 | // RA authentication functions |
195 | 80 | ||
196 | public function raLogin($email) { |
81 | public function raLogin($email) { |
197 | return $this->getAuthContentStore()->raLogin($email); |
82 | $acs = $this->getAuthContentStore(); |
- | 83 | if (is_null($acs)) return; |
|
- | 84 | return $acs->raLogin($email); |
|
198 | } |
85 | } |
199 | 86 | ||
200 | public function raLogout($email) { |
87 | public function raLogout($email) { |
201 | return $this->getAuthContentStore()->raLogout($email); |
88 | $acs = $this->getAuthContentStore(); |
- | 89 | if (is_null($acs)) return; |
|
- | 90 | return $acs->raLogout($email); |
|
202 | } |
91 | } |
203 | 92 | ||
204 | public function raCheckPassword($ra_email, $password) { |
93 | public function raCheckPassword($ra_email, $password) { |
205 | $ra = new OIDplusRA($ra_email); |
94 | $ra = new OIDplusRA($ra_email); |
206 | 95 | ||
Line 216... | Line 105... | ||
216 | 105 | ||
217 | return false; |
106 | return false; |
218 | } |
107 | } |
219 | 108 | ||
220 | public function raNumLoggedIn() { |
109 | public function raNumLoggedIn() { |
221 | return $this->getAuthContentStore()->raNumLoggedIn(); |
110 | $acs = $this->getAuthContentStore(); |
222 | } |
- | |
223 | - | ||
224 | public function raLogoutAll() { |
111 | if (is_null($acs)) return 0; |
225 | return $this->getAuthContentStore()->raLogoutAll(); |
112 | return $acs->raNumLoggedIn(); |
226 | } |
113 | } |
227 | 114 | ||
228 | public function loggedInRaList() { |
115 | public function loggedInRaList() { |
229 | if (OIDplus::authUtils()->forceAllLoggedOut()) { |
116 | if ($this->forceAllLoggedOut()) { |
230 | return array(); |
117 | return array(); |
231 | } else { |
118 | } else { |
232 | return $this->getAuthContentStore()->loggedInRaList(); |
119 | $acs = $this->getAuthContentStore(); |
- | 120 | if (is_null($acs)) return array(); |
|
- | 121 | return $acs->loggedInRaList(); |
|
233 | } |
122 | } |
234 | } |
123 | } |
235 | 124 | ||
236 | public function isRaLoggedIn($email) { |
125 | public function isRaLoggedIn($email) { |
- | 126 | $acs = $this->getAuthContentStore(); |
|
- | 127 | if (is_null($acs)) return false; |
|
237 | return $this->getAuthContentStore()->isRaLoggedIn($email); |
128 | return $acs->isRaLoggedIn($email); |
- | 129 | } |
|
- | 130 | ||
- | 131 | // "High level" function including logging and checking for valid JWT alternations |
|
- | 132 | public function raLoginEx($email, $remember_me, $origin) { |
|
- | 133 | $loginfo = ''; |
|
- | 134 | $acs = $this->getAuthContentStore(); |
|
- | 135 | if (!is_null($acs)) { |
|
- | 136 | $acs->raLoginEx($email, $loginfo); |
|
- | 137 | $acs->activate(); |
|
- | 138 | } else { |
|
- | 139 | if ($remember_me) { |
|
- | 140 | if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_USER', true)) { |
|
- | 141 | throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_USER')); |
|
- | 142 | } |
|
- | 143 | $ttl = OIDplus::baseConfig()->getValue('JWT_TTL_LOGIN_USER', 10*365*24*60*60); |
|
- | 144 | $authSimulation = new OIDplusAuthContentStoreJWT(); |
|
- | 145 | $authSimulation->raLoginEx($email, $loginfo); |
|
- | 146 | $authSimulation->setValue('oidplus_generator', OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN); |
|
- | 147 | $authSimulation->setValue('exp', time()+$ttl); // JWT "exp" attribute |
|
- | 148 | $authSimulation->activate(); |
|
- | 149 | } else { |
|
- | 150 | $authSimulation = new OIDplusAuthContentStoreSession(); |
|
- | 151 | $authSimulation->raLoginEx($email, $loginfo); |
|
- | 152 | $authSimulation->activate(); |
|
- | 153 | } |
|
- | 154 | } |
|
- | 155 | $logmsg = "RA '$email' logged in"; |
|
- | 156 | if ($origin != '') $logmsg .= " via $origin"; |
|
- | 157 | if ($loginfo != '') $logmsg .= " ($loginfo)"; |
|
- | 158 | OIDplus::logger()->log("[OK]RA($email)!", $logmsg); |
|
- | 159 | } |
|
- | 160 | ||
- | 161 | public function raLogoutEx($email) { |
|
- | 162 | $loginfo = ''; |
|
- | 163 | ||
- | 164 | $acs = $this->getAuthContentStore(); |
|
- | 165 | if (is_null($acs)) return; |
|
- | 166 | $res = $acs->raLogoutEx($email, $loginfo); |
|
- | 167 | ||
- | 168 | OIDplus::logger()->log("[OK]RA($email)!", "RA '$email' logged out ($loginfo)"); |
|
- | 169 | ||
- | 170 | if (($this->raNumLoggedIn() == 0) && (!$this->isAdminLoggedIn())) { |
|
- | 171 | // Nobody logged in anymore. Destroy session cookie to make GDPR people happy |
|
- | 172 | $acs->destroySession(); |
|
- | 173 | } else { |
|
- | 174 | // Get a new token for the remaining users |
|
- | 175 | $acs->activate(); |
|
- | 176 | } |
|
- | 177 | ||
- | 178 | return $res; |
|
238 | } |
179 | } |
239 | 180 | ||
240 | // Admin authentication functions |
181 | // Admin authentication functions |
241 | 182 | ||
242 | public function adminLogin() { |
183 | public function adminLogin() { |
243 | return $this->getAuthContentStore()->adminLogin(); |
184 | $acs = $this->getAuthContentStore(); |
- | 185 | if (is_null($acs)) return; |
|
- | 186 | return $acs->adminLogin(); |
|
244 | } |
187 | } |
245 | 188 | ||
246 | public function adminLogout() { |
189 | public function adminLogout() { |
247 | return $this->getAuthContentStore()->adminLogout(); |
190 | $acs = $this->getAuthContentStore(); |
- | 191 | if (is_null($acs)) return; |
|
- | 192 | return $acs->adminLogout(); |
|
248 | } |
193 | } |
249 | 194 | ||
250 | public function adminCheckPassword($password) { |
195 | public function adminCheckPassword($password) { |
251 | $passwordData = OIDplus::baseConfig()->getValue('ADMIN_PASSWORD', ''); |
196 | $passwordData = OIDplus::baseConfig()->getValue('ADMIN_PASSWORD', ''); |
252 | if (empty($passwordData)) { |
197 | if (empty($passwordData)) { |
Line 268... | Line 213... | ||
268 | } |
213 | } |
269 | return strcmp(sha3_512($s_salt.$password, true), base64_decode($hash)) === 0; |
214 | return strcmp(sha3_512($s_salt.$password, true), base64_decode($hash)) === 0; |
270 | } |
215 | } |
271 | 216 | ||
272 | public function isAdminLoggedIn() { |
217 | public function isAdminLoggedIn() { |
273 | if (OIDplus::authUtils()->forceAllLoggedOut()) { |
218 | if ($this->forceAllLoggedOut()) { |
274 | return false; |
219 | return false; |
275 | } else { |
220 | } else { |
276 | return $this->getAuthContentStore()->isAdminLoggedIn(); |
221 | $acs = $this->getAuthContentStore(); |
- | 222 | if (is_null($acs)) return false; |
|
- | 223 | return $acs->isAdminLoggedIn(); |
|
- | 224 | } |
|
- | 225 | } |
|
- | 226 | ||
- | 227 | // "High level" function including logging and checking for valid JWT alternations |
|
- | 228 | public function adminLoginEx($remember_me, $origin) { |
|
- | 229 | $loginfo = ''; |
|
- | 230 | $acs = $this->getAuthContentStore(); |
|
- | 231 | if (!is_null($acs)) { |
|
- | 232 | $acs->adminLoginEx($loginfo); |
|
- | 233 | $acs->activate(); |
|
- | 234 | } else { |
|
- | 235 | if ($remember_me) { |
|
- | 236 | if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) { |
|
- | 237 | throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_ADMIN')); |
|
- | 238 | } |
|
- | 239 | $ttl = OIDplus::baseConfig()->getValue('JWT_TTL_LOGIN_ADMIN', 10*365*24*60*60); |
|
- | 240 | $authSimulation = new OIDplusAuthContentStoreJWT(); |
|
- | 241 | $authSimulation->adminLoginEx($loginfo); |
|
- | 242 | $authSimulation->setValue('oidplus_generator', OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN); |
|
- | 243 | $authSimulation->setValue('exp', time()+$ttl); // JWT "exp" attribute |
|
- | 244 | $authSimulation->activate(); |
|
- | 245 | } else { |
|
- | 246 | $authSimulation = new OIDplusAuthContentStoreSession(); |
|
- | 247 | $authSimulation->adminLoginEx($loginfo); |
|
- | 248 | $authSimulation->activate(); |
|
- | 249 | } |
|
- | 250 | } |
|
- | 251 | $logmsg = "Admin logged in"; |
|
- | 252 | if ($origin != '') $logmsg .= " via $origin"; |
|
- | 253 | if ($loginfo != '') $logmsg .= " ($loginfo)"; |
|
- | 254 | OIDplus::logger()->log("[OK]A!", $logmsg); |
|
277 | } |
255 | } |
- | 256 | ||
- | 257 | public function adminLogoutEx() { |
|
- | 258 | $loginfo = ''; |
|
- | 259 | ||
- | 260 | $acs = $this->getAuthContentStore(); |
|
- | 261 | if (is_null($acs)) return; |
|
- | 262 | $res = $acs->adminLogoutEx($loginfo); |
|
- | 263 | ||
- | 264 | if ($this->raNumLoggedIn() == 0) { |
|
- | 265 | // Nobody here anymore. Destroy the cookie to make GDPR people happy |
|
- | 266 | $acs->destroySession(); |
|
- | 267 | } else { |
|
- | 268 | // Get a new token for the remaining users |
|
- | 269 | $acs->activate(); |
|
- | 270 | } |
|
- | 271 | ||
- | 272 | OIDplus::logger()->log("[OK]A!", "Admin logged out ($loginfo)"); |
|
- | 273 | return $res; |
|
278 | } |
274 | } |
279 | 275 | ||
280 | // Authentication keys for validating arguments (e.g. sent by mail) |
276 | // Authentication keys for validating arguments (e.g. sent by mail) |
281 | 277 | ||
282 | public static function makeAuthKey($data) { |
278 | public static function makeAuthKey($data) { |
Line 289... | Line 285... | ||
289 | return strcmp(self::makeAuthKey($data), $auth_key) === 0; |
285 | return strcmp(self::makeAuthKey($data), $auth_key) === 0; |
290 | } |
286 | } |
291 | 287 | ||
292 | // "Veto" functions to force logout state |
288 | // "Veto" functions to force logout state |
293 | 289 | ||
294 | public static function forceAllLoggedOut() { |
290 | protected function forceAllLoggedOut() { |
295 | if (isset($_SERVER['SCRIPT_FILENAME']) && (basename($_SERVER['SCRIPT_FILENAME']) == 'sitemap.php')) { |
291 | if (isset($_SERVER['SCRIPT_FILENAME']) && (basename($_SERVER['SCRIPT_FILENAME']) == 'sitemap.php')) { |
296 | // The sitemap may not contain any confidential information, |
292 | // The sitemap may not contain any confidential information, |
297 | // even if the user is logged in, because the admin could |
293 | // even if the user is logged in, because the admin could |
298 | // accidentally copy-paste the sitemap to a |
294 | // accidentally copy-paste the sitemap to a |
299 | // search engine control panel while they are logged in |
295 | // search engine control panel while they are logged in |
Line 340... | Line 336... | ||
340 | throw new OIDplusException(_L('Default RA auth method/plugin "%1" not found',$def_method)); |
336 | throw new OIDplusException(_L('Default RA auth method/plugin "%1" not found',$def_method)); |
341 | } |
337 | } |
342 | 338 | ||
343 | // Generate admin password |
339 | // Generate admin password |
344 | 340 | ||
345 | /* Nothing here; the admin password will be generated in setup_base.js */ |
341 | /* Nothing here; the admin password will be generated in setup_base.js , purely in the web-browser */ |
346 | 342 | ||
347 | } |
343 | } |