Rev 1300 | Rev 1303 | Go to most recent revision | Only display areas with differences | Regard whitespace | Details | Blame | Last modification | View Log | RSS feed
Rev 1300 | Rev 1301 | ||
---|---|---|---|
1 | <?php |
1 | <?php |
2 | 2 | ||
3 | /* |
3 | /* |
4 | * OIDplus 2.0 |
4 | * OIDplus 2.0 |
5 | * Copyright 2019 - 2023 Daniel Marschall, ViaThinkSoft |
5 | * Copyright 2019 - 2023 Daniel Marschall, ViaThinkSoft |
6 | * |
6 | * |
7 | * Licensed under the Apache License, Version 2.0 (the "License"); |
7 | * Licensed under the Apache License, Version 2.0 (the "License"); |
8 | * you may not use this file except in compliance with 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 |
9 | * You may obtain a copy of the License at |
10 | * |
10 | * |
11 | * http://www.apache.org/licenses/LICENSE-2.0 |
11 | * http://www.apache.org/licenses/LICENSE-2.0 |
12 | * |
12 | * |
13 | * Unless required by applicable law or agreed to in writing, software |
13 | * Unless required by applicable law or agreed to in writing, software |
14 | * distributed under the License is distributed on an "AS IS" BASIS, |
14 | * distributed under the License is distributed on an "AS IS" BASIS, |
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
16 | * See the License for the specific language governing permissions and |
16 | * See the License for the specific language governing permissions and |
17 | * limitations under the License. |
17 | * limitations under the License. |
18 | */ |
18 | */ |
19 | 19 | ||
20 | namespace ViaThinkSoft\OIDplus; |
20 | namespace ViaThinkSoft\OIDplus; |
21 | 21 | ||
22 | // phpcs:disable PSR1.Files.SideEffects |
22 | // phpcs:disable PSR1.Files.SideEffects |
23 | \defined('INSIDE_OIDPLUS') or die; |
23 | \defined('INSIDE_OIDPLUS') or die; |
24 | // phpcs:enable PSR1.Files.SideEffects |
24 | // phpcs:enable PSR1.Files.SideEffects |
25 | 25 | ||
26 | class OIDplusAuthUtils extends OIDplusBaseClass { |
26 | class OIDplusAuthUtils extends OIDplusBaseClass { |
27 | 27 | ||
28 | // Useful functions |
28 | // Useful functions |
29 | 29 | ||
30 | /** |
30 | /** |
31 | * @param string $password |
31 | * @param string $password |
32 | * @return string |
32 | * @return string |
33 | * @throws OIDplusException |
33 | * @throws OIDplusException |
34 | */ |
34 | */ |
35 | private function raPepperProcessing(string $password): string { |
35 | private function raPepperProcessing(string $password): string { |
36 | // Additional feature: Pepper |
36 | // Additional feature: Pepper |
37 | // The pepper is stored inside the base configuration file |
37 | // The pepper is stored inside the base configuration file |
38 | // It prevents that an attacker with SQL write rights can |
38 | // It prevents that an attacker with SQL write rights can |
39 | // create accounts. |
39 | // create accounts. |
40 | // ATTENTION!!! If a pepper is used, then the |
40 | // ATTENTION!!! If a pepper is used, then the |
41 | // hashes are bound to that pepper. If you change the pepper, |
41 | // hashes are bound to that pepper. If you change the pepper, |
42 | // then ALL passwords of RAs become INVALID! |
42 | // then ALL passwords of RAs become INVALID! |
43 | $pepper = OIDplus::baseConfig()->getValue('RA_PASSWORD_PEPPER',''); |
43 | $pepper = OIDplus::baseConfig()->getValue('RA_PASSWORD_PEPPER',''); |
44 | if ($pepper !== '') { |
44 | if ($pepper !== '') { |
45 | $algo = OIDplus::baseConfig()->getValue('RA_PASSWORD_PEPPER_ALGO','sha512'); // sha512 works with PHP 7.0 |
45 | $algo = OIDplus::baseConfig()->getValue('RA_PASSWORD_PEPPER_ALGO','sha512'); // sha512 works with PHP 7.0 |
46 | if (strtolower($algo) === 'sha3-512') { |
46 | if (strtolower($algo) === 'sha3-512') { |
47 | $hmac = sha3_512_hmac($password, $pepper); |
47 | $hmac = sha3_512_hmac($password, $pepper); |
48 | } else { |
48 | } else { |
49 | $hmac = hash_hmac($algo, $password, $pepper); |
49 | $hmac = hash_hmac($algo, $password, $pepper); |
50 | } |
50 | } |
51 | if ($hmac === "") throw new OIDplusException(_L('HMAC failed')); |
51 | if ($hmac === "") throw new OIDplusException(_L('HMAC failed')); |
52 | return $hmac; |
52 | return $hmac; |
53 | } else { |
53 | } else { |
54 | return $password; |
54 | return $password; |
55 | } |
55 | } |
56 | } |
56 | } |
57 | 57 | ||
58 | // Content provider |
58 | // Content provider |
59 | 59 | ||
60 | /** |
60 | /** |
61 | * @return string |
61 | * @return string |
62 | * @throws OIDplusException |
62 | * @throws OIDplusException |
63 | */ |
63 | */ |
64 | public function getAuthMethod(): string { |
64 | public function getAuthMethod(): string { |
65 | $acs = $this->getAuthContentStore(); |
65 | $acs = $this->getAuthContentStore(); |
66 | if (is_null($acs)) return 'null'; |
66 | if (is_null($acs)) return 'null'; |
67 | return get_class($acs); |
67 | return get_class($acs); |
68 | } |
68 | } |
69 | 69 | ||
70 | /** |
70 | /** |
71 | * @return OIDplusAuthContentStore|null |
71 | * @return OIDplusAuthContentStore|null |
72 | * @throws OIDplusException |
72 | * @throws OIDplusException |
73 | */ |
73 | */ |
74 | protected function getAuthContentStore()/*: ?OIDplusAuthContentStore*/ { |
74 | protected function getAuthContentStore()/*: ?OIDplusAuthContentStore*/ { |
75 | // TODO: Should we implement these AuthContentStore as plugin type, so that there can be more than just JWT and PHP session? |
75 | // TODO: Should we implement these AuthContentStore as plugin type, so that there can be more than just JWT and PHP session? |
76 | 76 | ||
77 | // Logged in via JWT |
77 | // Logged in via JWT |
78 | // (The JWT can come from a REST Authentication Bearer, an AJAX Cookie, or an Automated AJAX Call GET/POST token.) |
78 | // (The JWT can come from a REST Authentication Bearer, an AJAX Cookie, or an Automated AJAX Call GET/POST token.) |
79 | $tmp = OIDplusAuthContentStoreJWT::getActiveProvider(); |
79 | $tmp = OIDplusAuthContentStoreJWT::getActiveProvider(); |
80 | if ($tmp) return $tmp; |
80 | if ($tmp) return $tmp; |
81 | 81 | ||
82 | // Normal login via web-browser |
82 | // Normal login via web-browser |
83 | // Cookie will only be created once content is stored |
83 | // Cookie will only be created once content is stored |
84 | $tmp = OIDplusAuthContentStoreSession::getActiveProvider(); |
84 | $tmp = OIDplusAuthContentStoreSession::getActiveProvider(); |
85 | if ($tmp) return $tmp; |
85 | if ($tmp) return $tmp; |
86 | 86 | ||
87 | // No active session and no JWT token available. User is not logged in. |
87 | // No active session and no JWT token available. User is not logged in. |
88 | return null; |
88 | return null; |
89 | } |
89 | } |
90 | 90 | ||
91 | /** |
91 | /** |
92 | * @param string $name |
92 | * @param string $name |
93 | * @param mixed|null $default |
93 | * @param mixed|null $default |
94 | * @return mixed |
94 | * @return mixed |
95 | * @throws OIDplusException |
95 | * @throws OIDplusException |
96 | */ |
96 | */ |
97 | public function getExtendedAttribute(string $name, $default=NULL) { |
97 | public function getExtendedAttribute(string $name, $default=NULL) { |
98 | $acs = $this->getAuthContentStore(); |
98 | $acs = $this->getAuthContentStore(); |
99 | if (is_null($acs)) return $default; |
99 | if (is_null($acs)) return $default; |
100 | return $acs->getValue($name, $default); |
100 | return $acs->getValue($name, $default); |
101 | } |
101 | } |
102 | 102 | ||
103 | // RA authentication functions |
103 | // RA authentication functions |
104 | 104 | ||
105 | /** |
105 | /** |
106 | * @param string $email |
106 | * @param string $email |
107 | * @return void |
107 | * @return void |
108 | * @throws OIDplusException |
108 | * @throws OIDplusException |
109 | */ |
109 | */ |
110 | public function raLogin(string $email) { |
110 | public function raLogin(string $email) { |
111 | $acs = $this->getAuthContentStore(); |
111 | $acs = $this->getAuthContentStore(); |
112 | if (is_null($acs)) return; |
112 | if (is_null($acs)) return; |
113 | $acs->raLogin($email); |
113 | $acs->raLogin($email); |
114 | } |
114 | } |
115 | 115 | ||
116 | /** |
116 | /** |
117 | * @param string $email |
117 | * @param string $email |
118 | * @return void |
118 | * @return void |
119 | * @throws OIDplusException |
119 | * @throws OIDplusException |
120 | */ |
120 | */ |
121 | public function raLogout(string $email) { |
121 | public function raLogout(string $email) { |
122 | $acs = $this->getAuthContentStore(); |
122 | $acs = $this->getAuthContentStore(); |
123 | if (is_null($acs)) return; |
123 | if (is_null($acs)) return; |
124 | $acs->raLogout($email); |
124 | $acs->raLogout($email); |
125 | } |
125 | } |
126 | 126 | ||
127 | /** |
127 | /** |
128 | * @param string $ra_email |
128 | * @param string $ra_email |
129 | * @param string $password |
129 | * @param string $password |
130 | * @return bool |
130 | * @return bool |
131 | * @throws OIDplusException |
131 | * @throws OIDplusException |
132 | */ |
132 | */ |
133 | public function raCheckPassword(string $ra_email, string $password): bool { |
133 | public function raCheckPassword(string $ra_email, string $password): bool { |
134 | $ra = new OIDplusRA($ra_email); |
134 | $ra = new OIDplusRA($ra_email); |
135 | 135 | ||
136 | // Get RA info from RA |
136 | // Get RA info from RA |
137 | $authInfo = $ra->getAuthInfo(); |
137 | $authInfo = $ra->getAuthInfo(); |
138 | if (!$authInfo) return false; // user not found |
138 | if (!$authInfo) return false; // user not found |
139 | 139 | ||
140 | // Ask plugins if they can verify this hash |
140 | // Ask plugins if they can verify this hash |
141 | $plugins = OIDplus::getAuthPlugins(); |
141 | $plugins = OIDplus::getAuthPlugins(); |
142 | if (count($plugins) == 0) { |
142 | if (count($plugins) == 0) { |
143 | throw new OIDplusException(_L('No RA authentication plugins found')); |
143 | throw new OIDplusException(_L('No RA authentication plugins found')); |
144 | } |
144 | } |
145 | foreach ($plugins as $plugin) { |
145 | foreach ($plugins as $plugin) { |
146 | if ($plugin->verify($authInfo, $this->raPepperProcessing($password))) return true; |
146 | if ($plugin->verify($authInfo, $this->raPepperProcessing($password))) return true; |
147 | } |
147 | } |
148 | 148 | ||
149 | return false; |
149 | return false; |
150 | } |
150 | } |
151 | 151 | ||
152 | /** |
152 | /** |
153 | * @return int |
153 | * @return int |
154 | * @throws OIDplusException |
154 | * @throws OIDplusException |
155 | */ |
155 | */ |
156 | public function raNumLoggedIn(): int { |
156 | public function raNumLoggedIn(): int { |
157 | $acs = $this->getAuthContentStore(); |
157 | $acs = $this->getAuthContentStore(); |
158 | if (is_null($acs)) return 0; |
158 | if (is_null($acs)) return 0; |
159 | return $acs->raNumLoggedIn(); |
159 | return $acs->raNumLoggedIn(); |
160 | } |
160 | } |
161 | 161 | ||
162 | /** |
162 | /** |
163 | * @return OIDplusRA[] |
163 | * @return OIDplusRA[] |
164 | * @throws OIDplusException |
164 | * @throws OIDplusException |
165 | */ |
165 | */ |
166 | public function loggedInRaList(): array { |
166 | public function loggedInRaList(): array { |
167 | if ($this->forceAllLoggedOut()) { |
167 | if ($this->forceAllLoggedOut()) { |
168 | return array(); |
168 | return array(); |
169 | } else { |
169 | } else { |
170 | $acs = $this->getAuthContentStore(); |
170 | $acs = $this->getAuthContentStore(); |
171 | if (is_null($acs)) return array(); |
171 | if (is_null($acs)) return array(); |
172 | return $acs->loggedInRaList(); |
172 | return $acs->loggedInRaList(); |
173 | } |
173 | } |
174 | } |
174 | } |
175 | 175 | ||
176 | /** |
176 | /** |
177 | * @param string|OIDplusRA $ra |
177 | * @param string|OIDplusRA $ra |
178 | * @return bool |
178 | * @return bool |
179 | * @throws OIDplusException |
179 | * @throws OIDplusException |
180 | */ |
180 | */ |
181 | public function isRaLoggedIn($ra): bool { |
181 | public function isRaLoggedIn($ra): bool { |
182 | $email = $ra instanceof OIDplusRA ? $ra->raEmail() : $ra; |
182 | $email = $ra instanceof OIDplusRA ? $ra->raEmail() : $ra; |
183 | $acs = $this->getAuthContentStore(); |
183 | $acs = $this->getAuthContentStore(); |
184 | if (is_null($acs)) return false; |
184 | if (is_null($acs)) return false; |
185 | return $acs->isRaLoggedIn($email); |
185 | return $acs->isRaLoggedIn($email); |
186 | } |
186 | } |
187 | 187 | ||
188 | // "High level" function including logging and checking for valid JWT alternations |
188 | // "High level" function including logging and checking for valid JWT alternations |
189 | 189 | ||
190 | /** |
190 | /** |
191 | * @param string $email |
191 | * @param string $email |
192 | * @param bool $remember_me |
192 | * @param bool $remember_me |
193 | * @param string $origin |
193 | * @param string $origin |
194 | * @return void |
194 | * @return void |
195 | * @throws OIDplusException |
195 | * @throws OIDplusException |
196 | */ |
196 | */ |
197 | public function raLoginEx(string $email, bool $remember_me, string $origin='') { |
197 | public function raLoginEx(string $email, bool $remember_me, string $origin='') { |
198 | $loginfo = ''; |
198 | $loginfo = ''; |
199 | $acs = $this->getAuthContentStore(); |
199 | $acs = $this->getAuthContentStore(); |
200 | if (!is_null($acs)) { |
200 | if (!is_null($acs)) { |
- | 201 | // User is already logged in (a session or JWT exists), so we modify their login status |
|
201 | $acs->raLoginEx($email, $loginfo); |
202 | $acs->raLoginEx($email, $loginfo); |
202 | $acs->activate(); |
203 | $acs->activate(); |
203 | } else { |
204 | } else { |
- | 205 | // No user is logged in (no session or JWT exists). We now create a auth content store and activate it (cookies will be set etc.) |
|
204 | if ($remember_me) { |
206 | if ($remember_me) { |
205 | if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_USER', true)) { |
207 | if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_USER', true)) { |
206 | throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_USER')); |
208 | throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_USER')); |
207 | } |
209 | } |
208 | $ttl = OIDplus::baseConfig()->getValue('JWT_TTL_LOGIN_USER', 10*365*24*60*60); |
210 | $ttl = OIDplus::baseConfig()->getValue('JWT_TTL_LOGIN_USER', 10*365*24*60*60); |
209 | $authSimulation = new OIDplusAuthContentStoreJWT(); |
211 | $authSimulation = new OIDplusAuthContentStoreJWT(); |
210 | $authSimulation->raLoginEx($email, $loginfo); |
212 | $authSimulation->raLoginEx($email, $loginfo); |
211 | $authSimulation->setValue('oidplus_generator', OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN); |
213 | $authSimulation->setValue('oidplus_generator', OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN); |
212 | $authSimulation->setValue('exp', time()+$ttl); // JWT "exp" attribute |
214 | $authSimulation->setValue('exp', time()+$ttl); // JWT "exp" attribute |
213 | $authSimulation->activate(); |
215 | $authSimulation->activate(); |
214 | } else { |
216 | } else { |
215 | $authSimulation = new OIDplusAuthContentStoreSession(); |
217 | $authSimulation = new OIDplusAuthContentStoreSession(); |
216 | $authSimulation->raLoginEx($email, $loginfo); |
218 | $authSimulation->raLoginEx($email, $loginfo); |
217 | $authSimulation->activate(); |
219 | $authSimulation->activate(); |
218 | } |
220 | } |
219 | } |
221 | } |
220 | $logmsg = "RA '$email' logged in"; |
222 | $logmsg = "RA '$email' logged in"; |
221 | if ($origin != '') $logmsg .= " via $origin"; |
223 | if ($origin != '') $logmsg .= " via $origin"; |
222 | if ($loginfo != '') $logmsg .= " ($loginfo)"; |
224 | if ($loginfo != '') $logmsg .= " ($loginfo)"; |
223 | OIDplus::logger()->log("V2:[OK]RA(%1)", "%2", $email, $logmsg); |
225 | OIDplus::logger()->log("V2:[OK]RA(%1)", "%2", $email, $logmsg); |
224 | } |
226 | } |
225 | 227 | ||
226 | /** |
228 | /** |
227 | * @param string $email |
229 | * @param string $email |
228 | * @return void |
230 | * @return void |
229 | * @throws OIDplusException |
231 | * @throws OIDplusException |
230 | */ |
232 | */ |
231 | public function raLogoutEx(string $email) { |
233 | public function raLogoutEx(string $email) { |
232 | $loginfo = ''; |
234 | $loginfo = ''; |
233 | 235 | ||
234 | $acs = $this->getAuthContentStore(); |
236 | $acs = $this->getAuthContentStore(); |
235 | if (is_null($acs)) return; |
237 | if (is_null($acs)) return; |
236 | $acs->raLogoutEx($email, $loginfo); |
238 | $acs->raLogoutEx($email, $loginfo); |
237 | 239 | ||
238 | OIDplus::logger()->log("V2:[OK]RA(%1)", "RA '%1' logged out (%2)", $email, $loginfo); |
240 | OIDplus::logger()->log("V2:[OK]RA(%1)", "RA '%1' logged out (%2)", $email, $loginfo); |
239 | 241 | ||
240 | if (($this->raNumLoggedIn() == 0) && (!$this->isAdminLoggedIn())) { |
242 | if (($this->raNumLoggedIn() == 0) && (!$this->isAdminLoggedIn())) { |
241 | // Nobody logged in anymore. Destroy session cookie to make GDPR people happy |
243 | // Nobody logged in anymore. Destroy session cookie to make GDPR people happy |
242 | $acs->destroySession(); |
244 | $acs->destroySession(); |
243 | } else { |
245 | } else { |
244 | // Get a new token for the remaining users |
246 | // Get a new token for the remaining users |
245 | $acs->activate(); |
247 | $acs->activate(); |
246 | } |
248 | } |
247 | } |
249 | } |
248 | 250 | ||
249 | // Admin authentication functions |
251 | // Admin authentication functions |
250 | 252 | ||
251 | /** |
253 | /** |
252 | * @return void |
254 | * @return void |
253 | * @throws OIDplusException |
255 | * @throws OIDplusException |
254 | */ |
256 | */ |
255 | public function adminLogin() { |
257 | public function adminLogin() { |
256 | $acs = $this->getAuthContentStore(); |
258 | $acs = $this->getAuthContentStore(); |
257 | if (is_null($acs)) return; |
259 | if (is_null($acs)) return; |
258 | $acs->adminLogin(); |
260 | $acs->adminLogin(); |
259 | } |
261 | } |
260 | 262 | ||
261 | /** |
263 | /** |
262 | * @return void |
264 | * @return void |
263 | * @throws OIDplusException |
265 | * @throws OIDplusException |
264 | */ |
266 | */ |
265 | public function adminLogout() { |
267 | public function adminLogout() { |
266 | $acs = $this->getAuthContentStore(); |
268 | $acs = $this->getAuthContentStore(); |
267 | if (is_null($acs)) return; |
269 | if (is_null($acs)) return; |
268 | $acs->adminLogout(); |
270 | $acs->adminLogout(); |
269 | } |
271 | } |
270 | 272 | ||
271 | /** |
273 | /** |
272 | * @param string $password |
274 | * @param string $password |
273 | * @return bool |
275 | * @return bool |
274 | * @throws OIDplusException |
276 | * @throws OIDplusException |
275 | */ |
277 | */ |
276 | public function adminCheckPassword(string $password): bool { |
278 | public function adminCheckPassword(string $password): bool { |
277 | $cfgData = OIDplus::baseConfig()->getValue('ADMIN_PASSWORD', ''); |
279 | $cfgData = OIDplus::baseConfig()->getValue('ADMIN_PASSWORD', ''); |
278 | if (empty($cfgData)) { |
280 | if (empty($cfgData)) { |
279 | throw new OIDplusException(_L('No admin password set in %1','userdata/baseconfig/config.inc.php')); |
281 | throw new OIDplusException(_L('No admin password set in %1','userdata/baseconfig/config.inc.php')); |
280 | } |
282 | } |
281 | 283 | ||
282 | if (!is_array($cfgData)) { |
284 | if (!is_array($cfgData)) { |
283 | $passwordDataArray = array($cfgData); |
285 | $passwordDataArray = array($cfgData); |
284 | } else { |
286 | } else { |
285 | $passwordDataArray = $cfgData; // Multiple Administrator passwords |
287 | $passwordDataArray = $cfgData; // Multiple Administrator passwords |
286 | } |
288 | } |
287 | 289 | ||
288 | foreach ($passwordDataArray as $passwordData) { |
290 | foreach ($passwordDataArray as $passwordData) { |
289 | if (str_starts_with($passwordData, '$')) { |
291 | if (str_starts_with($passwordData, '$')) { |
290 | // Version 3: BCrypt (or any other crypt) |
292 | // Version 3: BCrypt (or any other crypt) |
291 | $ok = password_verify($password, $passwordData); |
293 | $ok = password_verify($password, $passwordData); |
292 | } else if (strpos($passwordData, '$') !== false) { |
294 | } else if (strpos($passwordData, '$') !== false) { |
293 | // Version 2: SHA3-512 with salt |
295 | // Version 2: SHA3-512 with salt |
294 | list($salt, $hash) = explode('$', $passwordData, 2); |
296 | list($salt, $hash) = explode('$', $passwordData, 2); |
295 | $ok = hash_equals(sha3_512($salt.$password, true), base64_decode($hash)); |
297 | $ok = hash_equals(sha3_512($salt.$password, true), base64_decode($hash)); |
296 | } else { |
298 | } else { |
297 | // Version 1: SHA3-512 without salt |
299 | // Version 1: SHA3-512 without salt |
298 | $ok = hash_equals(sha3_512($password, true), base64_decode($passwordData)); |
300 | $ok = hash_equals(sha3_512($password, true), base64_decode($passwordData)); |
299 | } |
301 | } |
300 | if ($ok) return true; |
302 | if ($ok) return true; |
301 | } |
303 | } |
302 | 304 | ||
303 | return false; |
305 | return false; |
304 | } |
306 | } |
305 | 307 | ||
306 | /** |
308 | /** |
307 | * @return bool |
309 | * @return bool |
308 | * @throws OIDplusException |
310 | * @throws OIDplusException |
309 | */ |
311 | */ |
310 | public function isAdminLoggedIn(): bool { |
312 | public function isAdminLoggedIn(): bool { |
311 | if ($this->forceAllLoggedOut()) { |
313 | if ($this->forceAllLoggedOut()) { |
312 | return false; |
314 | return false; |
313 | } else { |
315 | } else { |
314 | $acs = $this->getAuthContentStore(); |
316 | $acs = $this->getAuthContentStore(); |
315 | if (is_null($acs)) return false; |
317 | if (is_null($acs)) return false; |
316 | return $acs->isAdminLoggedIn(); |
318 | return $acs->isAdminLoggedIn(); |
317 | } |
319 | } |
318 | } |
320 | } |
319 | 321 | ||
320 | /** |
322 | /** |
321 | * "High level" function including logging and checking for valid JWT alternations |
323 | * "High level" function including logging and checking for valid JWT alternations |
322 | * @param bool $remember_me |
324 | * @param bool $remember_me |
323 | * @param string $origin |
325 | * @param string $origin |
324 | * @return void |
326 | * @return void |
325 | * @throws OIDplusException |
327 | * @throws OIDplusException |
326 | */ |
328 | */ |
327 | public function adminLoginEx(bool $remember_me, string $origin='') { |
329 | public function adminLoginEx(bool $remember_me, string $origin='') { |
328 | $loginfo = ''; |
330 | $loginfo = ''; |
329 | $acs = $this->getAuthContentStore(); |
331 | $acs = $this->getAuthContentStore(); |
330 | if (!is_null($acs)) { |
332 | if (!is_null($acs)) { |
- | 333 | // User is already logged in (a session or JWT exists), so we modify their login status |
|
331 | $acs->adminLoginEx($loginfo); |
334 | $acs->adminLoginEx($loginfo); |
332 | $acs->activate(); |
335 | $acs->activate(); |
333 | } else { |
336 | } else { |
- | 337 | // No user is logged in (no session or JWT exists). We now create a auth content store and activate it (cookies will be set etc.) |
|
334 | if ($remember_me) { |
338 | if ($remember_me) { |
335 | if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) { |
339 | if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) { |
336 | throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_ADMIN')); |
340 | throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_ADMIN')); |
337 | } |
341 | } |
338 | $ttl = OIDplus::baseConfig()->getValue('JWT_TTL_LOGIN_ADMIN', 10*365*24*60*60); |
342 | $ttl = OIDplus::baseConfig()->getValue('JWT_TTL_LOGIN_ADMIN', 10*365*24*60*60); |
339 | $authSimulation = new OIDplusAuthContentStoreJWT(); |
343 | $authSimulation = new OIDplusAuthContentStoreJWT(); |
340 | $authSimulation->adminLoginEx($loginfo); |
344 | $authSimulation->adminLoginEx($loginfo); |
341 | $authSimulation->setValue('oidplus_generator', OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN); |
345 | $authSimulation->setValue('oidplus_generator', OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN); |
342 | $authSimulation->setValue('exp', time()+$ttl); // JWT "exp" attribute |
346 | $authSimulation->setValue('exp', time()+$ttl); // JWT "exp" attribute |
343 | $authSimulation->activate(); |
347 | $authSimulation->activate(); |
344 | } else { |
348 | } else { |
345 | $authSimulation = new OIDplusAuthContentStoreSession(); |
349 | $authSimulation = new OIDplusAuthContentStoreSession(); |
346 | $authSimulation->adminLoginEx($loginfo); |
350 | $authSimulation->adminLoginEx($loginfo); |
347 | $authSimulation->activate(); |
351 | $authSimulation->activate(); |
348 | } |
352 | } |
349 | } |
353 | } |
350 | $logmsg = "Admin logged in"; |
354 | $logmsg = "Admin logged in"; |
351 | if ($origin != '') $logmsg .= " via $origin"; |
355 | if ($origin != '') $logmsg .= " via $origin"; |
352 | if ($loginfo != '') $logmsg .= " ($loginfo)"; |
356 | if ($loginfo != '') $logmsg .= " ($loginfo)"; |
353 | OIDplus::logger()->log("V2:[OK]A", "%1", $logmsg); |
357 | OIDplus::logger()->log("V2:[OK]A", "%1", $logmsg); |
354 | } |
358 | } |
355 | 359 | ||
356 | /** |
360 | /** |
357 | * @return void |
361 | * @return void |
358 | * @throws OIDplusException |
362 | * @throws OIDplusException |
359 | */ |
363 | */ |
360 | public function adminLogoutEx() { |
364 | public function adminLogoutEx() { |
361 | $loginfo = ''; |
365 | $loginfo = ''; |
362 | 366 | ||
363 | $acs = $this->getAuthContentStore(); |
367 | $acs = $this->getAuthContentStore(); |
364 | if (is_null($acs)) return; |
368 | if (is_null($acs)) return; |
365 | $acs->adminLogoutEx($loginfo); |
369 | $acs->adminLogoutEx($loginfo); |
366 | 370 | ||
367 | if ($this->raNumLoggedIn() == 0) { |
371 | if ($this->raNumLoggedIn() == 0) { |
368 | // Nobody here anymore. Destroy the cookie to make GDPR people happy |
372 | // Nobody here anymore. Destroy the cookie to make GDPR people happy |
369 | $acs->destroySession(); |
373 | $acs->destroySession(); |
370 | } else { |
374 | } else { |
371 | // Get a new token for the remaining users |
375 | // Get a new token for the remaining users |
372 | $acs->activate(); |
376 | $acs->activate(); |
373 | } |
377 | } |
374 | 378 | ||
375 | OIDplus::logger()->log("V2:[OK]A", "Admin logged out (%1)", $loginfo); |
379 | OIDplus::logger()->log("V2:[OK]A", "Admin logged out (%1)", $loginfo); |
376 | } |
380 | } |
377 | 381 | ||
378 | // Authentication keys for generating secrets or validating arguments (e.g. sent by mail) |
382 | // Authentication keys for generating secrets or validating arguments (e.g. sent by mail) |
379 | 383 | ||
380 | /** |
384 | /** |
381 | * @param array|string $data |
385 | * @param array|string $data |
382 | * @return string |
386 | * @return string |
383 | * @throws OIDplusException |
387 | * @throws OIDplusException |
384 | */ |
388 | */ |
385 | public function makeSecret($data): string { |
389 | public function makeSecret($data): string { |
386 | if (!is_array($data)) $data = [$data]; |
390 | if (!is_array($data)) $data = [$data]; |
387 | $data = json_encode($data); |
391 | $data = json_encode($data); |
388 | return sha3_512_hmac($data, 'OIDplus:'.OIDplus::baseConfig()->getValue('SERVER_SECRET'), false); |
392 | return sha3_512_hmac($data, 'OIDplus:'.OIDplus::baseConfig()->getValue('SERVER_SECRET'), false); |
389 | } |
393 | } |
390 | 394 | ||
391 | /** |
395 | /** |
392 | * @param array|string $data Arbitary data to be validated later |
396 | * @param array|string $data Arbitary data to be validated later |
393 | * @return string A string that need to be validated with validateAuthKey |
397 | * @return string A string that need to be validated with validateAuthKey |
394 | * @throws OIDplusException |
398 | * @throws OIDplusException |
395 | */ |
399 | */ |
396 | public function makeAuthKey($data): string { |
400 | public function makeAuthKey($data): string { |
397 | if (!is_array($data)) $data = [$data]; |
401 | if (!is_array($data)) $data = [$data]; |
398 | $ts = time(); |
402 | $ts = time(); |
399 | $data_ext = [$ts, $data]; |
403 | $data_ext = [$ts, $data]; |
400 | $secret = $this->makeSecret($data_ext); |
404 | $secret = $this->makeSecret($data_ext); |
401 | return $ts.'.'.$secret; |
405 | return $ts.'.'.$secret; |
402 | } |
406 | } |
403 | 407 | ||
404 | /** |
408 | /** |
405 | * @param array|string $data The original data that had been passed to makeAuthKey() |
409 | * @param array|string $data The original data that had been passed to makeAuthKey() |
406 | * @param string $auth_key The result from makeAuthKey() |
410 | * @param string $auth_key The result from makeAuthKey() |
407 | * @param int $valid_secs How many seconds is the auth key valid? (-1 for infinite) |
411 | * @param int $valid_secs How many seconds is the auth key valid? (-1 for infinite) |
408 | * @return bool True if the key is valid and not expired. |
412 | * @return bool True if the key is valid and not expired. |
409 | * @throws OIDplusException |
413 | * @throws OIDplusException |
410 | */ |
414 | */ |
411 | public function validateAuthKey($data, string $auth_key, int $valid_secs=-1): bool { |
415 | public function validateAuthKey($data, string $auth_key, int $valid_secs=-1): bool { |
412 | $auth_key_ary = explode('.', $auth_key, 2); |
416 | $auth_key_ary = explode('.', $auth_key, 2); |
413 | if (count($auth_key_ary) != 2) return false; // invalid auth key syntax |
417 | if (count($auth_key_ary) != 2) return false; // invalid auth key syntax |
414 | list($ts, $secret) = $auth_key_ary; |
418 | list($ts, $secret) = $auth_key_ary; |
415 | if (!is_numeric($ts)) return false; // invalid auth key syntax |
419 | if (!is_numeric($ts)) return false; // invalid auth key syntax |
416 | if ($valid_secs >= 0) { |
420 | if ($valid_secs >= 0) { |
417 | if (time() > ($ts+$valid_secs)) return false; // expired auth key |
421 | if (time() > ($ts+$valid_secs)) return false; // expired auth key |
418 | } |
422 | } |
419 | if (!is_array($data)) $data = [$data]; |
423 | if (!is_array($data)) $data = [$data]; |
420 | $data_ext = [(int)$ts, $data]; |
424 | $data_ext = [(int)$ts, $data]; |
421 | return hash_equals($this->makeSecret($data_ext), $secret); |
425 | return hash_equals($this->makeSecret($data_ext), $secret); |
422 | } |
426 | } |
423 | 427 | ||
424 | // "Veto" functions to force logout state |
428 | // "Veto" functions to force logout state |
425 | 429 | ||
426 | /** |
430 | /** |
427 | * @return bool |
431 | * @return bool |
428 | */ |
432 | */ |
429 | protected function forceAllLoggedOut(): bool { |
433 | protected function forceAllLoggedOut(): bool { |
430 | if (isset($_SERVER['SCRIPT_FILENAME']) && (basename($_SERVER['SCRIPT_FILENAME']) == 'sitemap.php')) { |
434 | if (isset($_SERVER['SCRIPT_FILENAME']) && (basename($_SERVER['SCRIPT_FILENAME']) == 'sitemap.php')) { |
431 | // The sitemap may not contain any confidential information, |
435 | // The sitemap may not contain any confidential information, |
432 | // even if the user is logged in, because the admin could |
436 | // even if the user is logged in, because the admin could |
433 | // accidentally copy-paste the sitemap to a |
437 | // accidentally copy-paste the sitemap to a |
434 | // search engine control panel while they are logged in |
438 | // search engine control panel while they are logged in |
435 | return true; |
439 | return true; |
436 | } else { |
440 | } else { |
437 | return false; |
441 | return false; |
438 | } |
442 | } |
439 | } |
443 | } |
440 | 444 | ||
441 | // CSRF functions |
445 | // CSRF functions |
442 | 446 | ||
443 | private $enable_csrf = true; |
447 | private $enable_csrf = true; |
444 | 448 | ||
445 | /** |
449 | /** |
446 | * @return void |
450 | * @return void |
447 | */ |
451 | */ |
448 | public function enableCSRF() { |
452 | public function enableCSRF() { |
449 | $this->enable_csrf = true; |
453 | $this->enable_csrf = true; |
450 | } |
454 | } |
451 | 455 | ||
452 | /** |
456 | /** |
453 | * @return void |
457 | * @return void |
454 | */ |
458 | */ |
455 | public function disableCSRF() { |
459 | public function disableCSRF() { |
456 | $this->enable_csrf = false; |
460 | $this->enable_csrf = false; |
457 | } |
461 | } |
458 | 462 | ||
459 | /** |
463 | /** |
460 | * @return string |
464 | * @return string |
461 | * @throws \Random\RandomException |
465 | * @throws \Random\RandomException |
462 | */ |
466 | */ |
463 | public function genCSRFToken(): string { |
467 | public function genCSRFToken(): string { |
464 | return random_bytes_ex(64, false, false); |
468 | return random_bytes_ex(64, false, false); |
465 | } |
469 | } |
466 | 470 | ||
467 | /** |
471 | /** |
468 | * @return void |
472 | * @return void |
469 | * @throws OIDplusException |
473 | * @throws OIDplusException |
470 | */ |
474 | */ |
471 | public function checkCSRF() { |
475 | public function checkCSRF() { |
472 | if (!$this->enable_csrf) return; |
476 | if (!$this->enable_csrf) return; |
473 | 477 | ||
474 | $request_token = $_REQUEST['csrf_token'] ?? ''; |
478 | $request_token = $_REQUEST['csrf_token'] ?? ''; |
475 | $cookie_token = $_COOKIE['csrf_token'] ?? ''; |
479 | $cookie_token = $_COOKIE['csrf_token'] ?? ''; |
476 | 480 | ||
477 | if (empty($request_token) || empty($cookie_token) || ($request_token !== $cookie_token)) { |
481 | if (empty($request_token) || empty($cookie_token) || ($request_token !== $cookie_token)) { |
478 | if (OIDplus::baseConfig()->getValue('DEBUG')) { |
482 | if (OIDplus::baseConfig()->getValue('DEBUG')) { |
479 | throw new OIDplusException(_L('Missing or wrong CSRF Token: Request %1 vs Cookie %2', |
483 | throw new OIDplusException(_L('Missing or wrong CSRF Token: Request %1 vs Cookie %2', |
480 | isset($_REQUEST['csrf_token']) ? '"'.$_REQUEST['csrf_token'].'"' : 'NULL', |
484 | isset($_REQUEST['csrf_token']) ? '"'.$_REQUEST['csrf_token'].'"' : 'NULL', |
481 | $_COOKIE['csrf_token'] ?? 'NULL' |
485 | $_COOKIE['csrf_token'] ?? 'NULL' |
482 | )); |
486 | )); |
483 | } else { |
487 | } else { |
484 | throw new OIDplusException(_L('Missing or wrong "CSRF Token". To fix the issue, try clearing your browser cache and reload the page. If you visited the page via HTTPS before, try HTTPS in case you are currently connected via HTTP.')); |
488 | throw new OIDplusException(_L('Missing or wrong "CSRF Token". To fix the issue, try clearing your browser cache and reload the page. If you visited the page via HTTPS before, try HTTPS in case you are currently connected via HTTP.')); |
485 | } |
489 | } |
486 | } |
490 | } |
487 | } |
491 | } |
488 | 492 | ||
489 | // Generate RA passwords |
493 | // Generate RA passwords |
490 | 494 | ||
491 | /** |
495 | /** |
492 | * @param string $password |
496 | * @param string $password |
493 | * @return OIDplusRAAuthInfo |
497 | * @return OIDplusRAAuthInfo |
494 | * @throws OIDplusException |
498 | * @throws OIDplusException |
495 | */ |
499 | */ |
496 | public function raGeneratePassword(string $password): OIDplusRAAuthInfo { |
500 | public function raGeneratePassword(string $password): OIDplusRAAuthInfo { |
497 | $plugin = OIDplus::getDefaultRaAuthPlugin(true); |
501 | $plugin = OIDplus::getDefaultRaAuthPlugin(true); |
498 | return $plugin->generate($this->raPepperProcessing($password)); |
502 | return $plugin->generate($this->raPepperProcessing($password)); |
499 | } |
503 | } |
500 | 504 | ||
501 | // Generate admin password |
505 | // Generate admin password |
502 | 506 | ||
503 | /* Nothing here; the admin password will be generated in setup_base.js , purely in the web-browser */ |
507 | /* Nothing here; the admin password will be generated in setup_base.js , purely in the web-browser */ |
504 | 508 | ||
505 | } |
509 | } |
506 | 510 |