Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
62.24% |
122 / 196 |
|
48.89% |
22 / 45 |
CRAP | |
0.00% |
0 / 1 |
Manager | |
62.24% |
122 / 196 |
|
48.89% |
22 / 45 |
614.87 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getAuth | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
makeAuth | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
supportsCreation | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
supportsRecovery | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
supportsEmailChange | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
supportsPasswordChange | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
supportsConnectingLibraryCard | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
supportsPersistentLogin | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
getPersistentLoginLifetime | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getUsernamePolicy | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getPasswordPolicy | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getSessionInitiator | |
16.67% |
1 / 6 |
|
0.00% |
0 / 1 |
8.21 | |||
getAuthClassForTemplateRendering | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
getSelectableAuthOptions | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
getLoginTargets | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getDefaultLoginTarget | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getAuthMethod | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getSelectedAuthMethod | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
loginEnabled | |
62.50% |
5 / 8 |
|
0.00% |
0 / 1 |
6.32 | |||
ajaxEnabled | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
dropdownEnabled | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
logout | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
2 | |||
userHasLoggedOut | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isLoggedIn | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getUserObject | |
50.00% |
7 / 14 |
|
0.00% |
0 / 1 |
13.12 | |||
getCsrfHash | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getIdentity | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
checkForExpiredCredentials | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
inPrivacyMode | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
updateSession | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
2.03 | |||
create | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
updatePassword | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
updateEmail | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
updateUserVerifyHash | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
login | |
83.33% |
25 / 30 |
|
0.00% |
0 / 1 |
12.67 | |||
deleteToken | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
deleteUserLoginTokens | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setAuthMethod | |
81.82% |
9 / 11 |
|
0.00% |
0 / 1 |
4.10 | |||
validateCredentials | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getILSLoginMethod | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
connectLibraryCard | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
updateUser | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
allowsUserIlsLogin | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
processPolicyConfig | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
30 |
1 | <?php |
2 | |
3 | /** |
4 | * Wrapper class for handling logged-in user in session. |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * Copyright (C) Villanova University 2007. |
9 | * |
10 | * This program is free software; you can redistribute it and/or modify |
11 | * it under the terms of the GNU General Public License version 2, |
12 | * as published by the Free Software Foundation. |
13 | * |
14 | * This program is distributed in the hope that it will be useful, |
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
17 | * GNU General Public License for more details. |
18 | * |
19 | * You should have received a copy of the GNU General Public License |
20 | * along with this program; if not, write to the Free Software |
21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
22 | * |
23 | * @category VuFind |
24 | * @package Authentication |
25 | * @author Demian Katz <demian.katz@villanova.edu> |
26 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
27 | * @link https://vufind.org Main Page |
28 | */ |
29 | |
30 | namespace VuFind\Auth; |
31 | |
32 | use Laminas\Config\Config; |
33 | use Laminas\Session\SessionManager; |
34 | use LmcRbacMvc\Identity\IdentityInterface; |
35 | use VuFind\Cookie\CookieManager; |
36 | use VuFind\Db\Entity\UserEntityInterface; |
37 | use VuFind\Db\Service\UserServiceInterface; |
38 | use VuFind\Exception\Auth as AuthException; |
39 | use VuFind\ILS\Connection; |
40 | use VuFind\Validator\CsrfInterface; |
41 | |
42 | use function in_array; |
43 | use function is_callable; |
44 | |
45 | /** |
46 | * Wrapper class for handling logged-in user in session. |
47 | * |
48 | * @category VuFind |
49 | * @package Authentication |
50 | * @author Demian Katz <demian.katz@villanova.edu> |
51 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
52 | * @link https://vufind.org Main Page |
53 | */ |
54 | class Manager implements |
55 | \LmcRbacMvc\Identity\IdentityProviderInterface, |
56 | \Laminas\Log\LoggerAwareInterface |
57 | { |
58 | use \VuFind\Log\LoggerAwareTrait; |
59 | |
60 | /** |
61 | * Authentication modules |
62 | * |
63 | * @var \VuFind\Auth\AbstractBase[] |
64 | */ |
65 | protected $auth = []; |
66 | |
67 | /** |
68 | * Currently selected authentication module |
69 | * |
70 | * @var string |
71 | */ |
72 | protected $activeAuth; |
73 | |
74 | /** |
75 | * List of values allowed to be set into $activeAuth |
76 | * |
77 | * @var array |
78 | */ |
79 | protected $legalAuthOptions; |
80 | |
81 | /** |
82 | * Cache for current logged in user object |
83 | * |
84 | * @var ?UserEntityInterface |
85 | */ |
86 | protected $currentUser = null; |
87 | |
88 | /** |
89 | * Cache for hideLogin setting |
90 | * |
91 | * @var ?bool |
92 | */ |
93 | protected $hideLogin = null; |
94 | |
95 | /** |
96 | * Constructor |
97 | * |
98 | * @param Config $config VuFind configuration |
99 | * @param UserServiceInterface $userService User database service |
100 | * @param UserSessionPersistenceInterface $userSession User session persistence service |
101 | * @param SessionManager $sessionManager Session manager |
102 | * @param PluginManager $pluginManager Authentication plugin manager |
103 | * @param CookieManager $cookieManager Cookie manager |
104 | * @param CsrfInterface $csrf CSRF validator |
105 | * @param LoginTokenManager $loginTokenManager Login Token manager |
106 | * @param Connection $ils ILS connection |
107 | */ |
108 | public function __construct( |
109 | protected Config $config, |
110 | protected UserServiceInterface $userService, |
111 | protected UserSessionPersistenceInterface $userSession, |
112 | protected SessionManager $sessionManager, |
113 | protected PluginManager $pluginManager, |
114 | protected CookieManager $cookieManager, |
115 | protected CsrfInterface $csrf, |
116 | protected LoginTokenManager $loginTokenManager, |
117 | protected Connection $ils |
118 | ) { |
119 | // Initialize active authentication setting (defaulting to Database |
120 | // if no setting passed in): |
121 | $method = $config->Authentication->method ?? 'Database'; |
122 | $this->legalAuthOptions = [$method]; // mark it as legal |
123 | $this->setAuthMethod($method); // load it |
124 | } |
125 | |
126 | /** |
127 | * Get the authentication handler. |
128 | * |
129 | * @param string $name Auth module to load (null for currently active one) |
130 | * |
131 | * @return AbstractBase |
132 | */ |
133 | protected function getAuth($name = null) |
134 | { |
135 | $name = empty($name) ? $this->activeAuth : $name; |
136 | if (!isset($this->auth[$name])) { |
137 | $this->auth[$name] = $this->makeAuth($name); |
138 | } |
139 | return $this->auth[$name]; |
140 | } |
141 | |
142 | /** |
143 | * Helper |
144 | * |
145 | * @param string $method auth method to instantiate |
146 | * |
147 | * @return AbstractBase |
148 | */ |
149 | protected function makeAuth($method) |
150 | { |
151 | $legalAuthList = array_map('strtolower', $this->legalAuthOptions); |
152 | // If an illegal option was passed in, don't allow the object to load: |
153 | if (!in_array(strtolower($method), $legalAuthList)) { |
154 | throw new \Exception("Illegal authentication method: $method"); |
155 | } |
156 | $auth = $this->pluginManager->get($method); |
157 | $auth->setConfig($this->config); |
158 | return $auth; |
159 | } |
160 | |
161 | /** |
162 | * Does the current configuration support account creation? |
163 | * |
164 | * @param string $authMethod optional; check this auth method rather than |
165 | * the one in config file |
166 | * |
167 | * @return bool |
168 | */ |
169 | public function supportsCreation($authMethod = null) |
170 | { |
171 | return $this->getAuth($authMethod)->supportsCreation(); |
172 | } |
173 | |
174 | /** |
175 | * Does the current configuration support password recovery? |
176 | * |
177 | * @param string $authMethod optional; check this auth method rather than |
178 | * the one in config file |
179 | * |
180 | * @return bool |
181 | */ |
182 | public function supportsRecovery($authMethod = null) |
183 | { |
184 | return ($this->config->Authentication->recover_password ?? false) |
185 | && $this->getAuth($authMethod)->supportsPasswordRecovery(); |
186 | } |
187 | |
188 | /** |
189 | * Is email changing currently allowed? |
190 | * |
191 | * @param string $authMethod optional; check this auth method rather than |
192 | * the one in config file |
193 | * |
194 | * @return bool |
195 | */ |
196 | public function supportsEmailChange($authMethod = null) |
197 | { |
198 | return $this->config->Authentication->change_email ?? false; |
199 | } |
200 | |
201 | /** |
202 | * Is new passwords currently allowed? |
203 | * |
204 | * @param string $authMethod optional; check this auth method rather than |
205 | * the one in config file |
206 | * |
207 | * @return bool |
208 | */ |
209 | public function supportsPasswordChange($authMethod = null) |
210 | { |
211 | return ($this->config->Authentication->change_password ?? false) |
212 | && $this->getAuth($authMethod)->supportsPasswordChange(); |
213 | } |
214 | |
215 | /** |
216 | * Is connecting library card allowed and supported? |
217 | * |
218 | * @param string $authMethod optional; check this auth method rather than |
219 | * the one in config file |
220 | * |
221 | * @return bool |
222 | */ |
223 | public function supportsConnectingLibraryCard($authMethod = null) |
224 | { |
225 | return ($this->config->Catalog->auth_based_library_cards ?? false) |
226 | && $this->getAuth($authMethod)->supportsConnectingLibraryCard(); |
227 | } |
228 | |
229 | /** |
230 | * Is persistent login supported by the authentication method? |
231 | * |
232 | * @param string $method Authentication method (overrides currently selected method) |
233 | * |
234 | * @return bool |
235 | */ |
236 | public function supportsPersistentLogin(?string $method = null): bool |
237 | { |
238 | if (!empty($this->config->Authentication->persistent_login)) { |
239 | return in_array( |
240 | strtolower($method ?? $this->getSelectedAuthMethod()), |
241 | explode(',', strtolower($this->config->Authentication->persistent_login)) |
242 | ); |
243 | } |
244 | return false; |
245 | } |
246 | |
247 | /** |
248 | * Get persistent login lifetime in days |
249 | * |
250 | * @return int |
251 | */ |
252 | public function getPersistentLoginLifetime() |
253 | { |
254 | return $this->config->Authentication->persistent_login_lifetime ?? 14; |
255 | } |
256 | |
257 | /** |
258 | * Username policy for a new account (e.g. minLength, maxLength) |
259 | * |
260 | * @param string $authMethod optional; check this auth method rather than |
261 | * the one in config file |
262 | * |
263 | * @return array |
264 | */ |
265 | public function getUsernamePolicy($authMethod = null) |
266 | { |
267 | return $this->processPolicyConfig( |
268 | $this->getAuth($authMethod)->getUsernamePolicy() |
269 | ); |
270 | } |
271 | |
272 | /** |
273 | * Password policy for a new password (e.g. minLength, maxLength) |
274 | * |
275 | * @param string $authMethod optional; check this auth method rather than |
276 | * the one in config file |
277 | * |
278 | * @return array |
279 | */ |
280 | public function getPasswordPolicy($authMethod = null) |
281 | { |
282 | return $this->processPolicyConfig( |
283 | $this->getAuth($authMethod)->getPasswordPolicy() |
284 | ); |
285 | } |
286 | |
287 | /** |
288 | * Get the URL to establish a session (needed when the internal VuFind login |
289 | * form is inadequate). Returns false when no session initiator is needed. |
290 | * |
291 | * @param string $target Full URL where external authentication method should |
292 | * send user after login (some drivers may override this). |
293 | * |
294 | * @return bool|string |
295 | */ |
296 | public function getSessionInitiator($target) |
297 | { |
298 | try { |
299 | return $this->getAuth()->getSessionInitiator($target); |
300 | } catch (InvalidArgumentException $e) { |
301 | // If the authentication is in an illegal state but there is an |
302 | // active user session, we should clear everything out so the user |
303 | // can try again. This is useful, for example, if a user is logged |
304 | // in at the same time that an administrator changes the [ChoiceAuth] |
305 | // settings in config.ini. However, if the user is not logged in, |
306 | // they are probably attempting something nasty and should be given |
307 | // an error message. |
308 | if (!$this->getIdentity()) { |
309 | throw $e; |
310 | } |
311 | $this->logout(''); |
312 | return $this->getAuth()->getSessionInitiator($target); |
313 | } |
314 | } |
315 | |
316 | /** |
317 | * In VuFind, views are tied to the name of the active authentication class. |
318 | * This method returns that name so that an appropriate template can be |
319 | * selected. It supports authentication methods that proxy other authentication |
320 | * methods (see ChoiceAuth for an example). |
321 | * |
322 | * @return string |
323 | */ |
324 | public function getAuthClassForTemplateRendering() |
325 | { |
326 | $auth = $this->getAuth(); |
327 | if (is_callable([$auth, 'getSelectedAuthOption'])) { |
328 | $selected = $auth->getSelectedAuthOption(); |
329 | if ($selected) { |
330 | $auth = $this->getAuth($selected); |
331 | } |
332 | } |
333 | return $auth::class; |
334 | } |
335 | |
336 | /** |
337 | * Return an array of all of the authentication options supported by the |
338 | * current auth class. In most cases (except for ChoiceAuth), this will |
339 | * just contain a single value. |
340 | * |
341 | * @return array |
342 | */ |
343 | public function getSelectableAuthOptions() |
344 | { |
345 | $auth = $this->getAuth(); |
346 | if (is_callable([$auth, 'getSelectableAuthOptions'])) { |
347 | if ($methods = $auth->getSelectableAuthOptions()) { |
348 | return $methods; |
349 | } |
350 | } |
351 | return [$this->getAuthMethod()]; |
352 | } |
353 | |
354 | /** |
355 | * Does the current auth class allow for authentication from more than |
356 | * one target? (e.g. MultiILS) |
357 | * If so return an array that lists the targets. |
358 | * |
359 | * @return array |
360 | */ |
361 | public function getLoginTargets() |
362 | { |
363 | $auth = $this->getAuth(); |
364 | return is_callable([$auth, 'getLoginTargets']) |
365 | ? $auth->getLoginTargets() : []; |
366 | } |
367 | |
368 | /** |
369 | * Does the current auth class allow for authentication from more than |
370 | * one target? (e.g. MultiILS) |
371 | * If so return the default target. |
372 | * |
373 | * @return string |
374 | */ |
375 | public function getDefaultLoginTarget() |
376 | { |
377 | $auth = $this->getAuth(); |
378 | return is_callable([$auth, 'getDefaultLoginTarget']) |
379 | ? $auth->getDefaultLoginTarget() : null; |
380 | } |
381 | |
382 | /** |
383 | * Get the name of the current authentication method. |
384 | * |
385 | * @return string |
386 | */ |
387 | public function getAuthMethod() |
388 | { |
389 | return $this->activeAuth; |
390 | } |
391 | |
392 | /** |
393 | * Get the name of the currently selected authentication method (if applicable) |
394 | * or the active authentication method. |
395 | * |
396 | * @return string |
397 | */ |
398 | public function getSelectedAuthMethod() |
399 | { |
400 | $auth = $this->getAuth(); |
401 | return is_callable([$auth, 'getSelectedAuthOption']) |
402 | ? $auth->getSelectedAuthOption() |
403 | : $this->getAuthMethod(); |
404 | } |
405 | |
406 | /** |
407 | * Is login currently allowed? |
408 | * |
409 | * @return bool |
410 | */ |
411 | public function loginEnabled() |
412 | { |
413 | if (null === $this->hideLogin) { |
414 | // Assume login is enabled unless explicitly turned off: |
415 | $this->hideLogin = ($this->config->Authentication->hideLogin ?? false); |
416 | |
417 | if (!$this->hideLogin) { |
418 | try { |
419 | // Check if the catalog wants to hide the login link, and override |
420 | // the configuration if necessary. |
421 | if ($this->ils->loginIsHidden()) { |
422 | $this->hideLogin = true; |
423 | } |
424 | } catch (\Exception $e) { |
425 | // Ignore exceptions; if the catalog is broken, throwing an exception |
426 | // here may interfere with UI rendering. If we ignore it now, it will |
427 | // still get handled appropriately later in processing. |
428 | $this->logError('Could not check loginIsHidden:' . (string)$e); |
429 | } |
430 | } |
431 | } |
432 | return !$this->hideLogin; |
433 | } |
434 | |
435 | /** |
436 | * Is login currently allowed? |
437 | * |
438 | * @return bool |
439 | */ |
440 | public function ajaxEnabled() |
441 | { |
442 | // Assume ajax is enabled unless explicitly turned off: |
443 | return $this->config->Authentication->enableAjax ?? true; |
444 | } |
445 | |
446 | /** |
447 | * Is login currently allowed? |
448 | * |
449 | * @return bool |
450 | */ |
451 | public function dropdownEnabled() |
452 | { |
453 | // Assume dropdown is disabled unless explicitly turned on: |
454 | return $this->config->Authentication->enableDropdown ?? false; |
455 | } |
456 | |
457 | /** |
458 | * Log out the current user. |
459 | * |
460 | * @param string $url URL to redirect user to after logging out. |
461 | * @param bool $destroy Should we destroy the session (true) or just reset it |
462 | * (false); destroy is for log out, reset is for expiration. |
463 | * |
464 | * @return string Redirect URL (usually same as $url, but modified in |
465 | * some authentication modules). |
466 | */ |
467 | public function logout($url, $destroy = true) |
468 | { |
469 | // Perform authentication-specific cleanup and modify redirect URL if |
470 | // necessary. |
471 | $url = $this->getAuth()->logout($url); |
472 | |
473 | // Reset authentication state |
474 | $this->getAuth()->resetState(); |
475 | |
476 | // Clear out the cached user object and session entry. |
477 | $this->currentUser = null; |
478 | $this->userSession->clearUserFromSession(); |
479 | $this->cookieManager->set('loggedOut', 1); |
480 | $this->loginTokenManager->deleteActiveToken(); |
481 | |
482 | // Destroy the session for good measure, if requested. |
483 | if ($destroy) { |
484 | $this->sessionManager->destroy(); |
485 | } else { |
486 | // If we don't want to destroy the session, we still need to empty it. |
487 | // There should be a way to do this through Laminas\Session, but there |
488 | // apparently isn't (TODO -- do this better): |
489 | $_SESSION = []; |
490 | } |
491 | |
492 | return $url; |
493 | } |
494 | |
495 | /** |
496 | * Checks whether the user has recently logged out. |
497 | * |
498 | * @return bool |
499 | */ |
500 | public function userHasLoggedOut() |
501 | { |
502 | return (bool)$this->cookieManager->get('loggedOut'); |
503 | } |
504 | |
505 | /** |
506 | * Checks whether the user is logged in. |
507 | * |
508 | * @return UserEntityInterface|false Object if user is logged in, false otherwise. |
509 | * |
510 | * @deprecated Use getIdentity() or getUserObject() instead. |
511 | */ |
512 | public function isLoggedIn() |
513 | { |
514 | return $this->getUserObject() ?? false; |
515 | } |
516 | |
517 | /** |
518 | * Checks whether the user is logged in. |
519 | * |
520 | * @return ?UserEntityInterface Object if user is logged in, null otherwise. |
521 | */ |
522 | public function getUserObject(): ?UserEntityInterface |
523 | { |
524 | // If user object is not in cache, but user ID is in session, |
525 | // load the object from the database: |
526 | if (!$this->currentUser) { |
527 | if ($this->userSession->hasUserSessionData()) { |
528 | $this->currentUser = $this->userSession->getUserFromSession(); |
529 | // End the session if the logged-in user cannot be found: |
530 | if (null === $this->currentUser) { |
531 | $this->logout(''); |
532 | } |
533 | } elseif ($user = $this->loginTokenManager->tokenLogin($this->sessionManager->getId())) { |
534 | if ($this->getAuth() instanceof ChoiceAuth) { |
535 | $this->getAuth()->setStrategy($user->getAuthMethod()); |
536 | } |
537 | if ($this->supportsPersistentLogin()) { |
538 | $this->updateUser($user, null); |
539 | $this->updateSession($user); |
540 | } else { |
541 | $this->currentUser = null; |
542 | } |
543 | } else { |
544 | // not logged in |
545 | $this->currentUser = null; |
546 | } |
547 | } |
548 | return $this->currentUser; |
549 | } |
550 | |
551 | /** |
552 | * Retrieve CSRF token |
553 | * |
554 | * If no CSRF token currently exists, or should be regenerated, generates one. |
555 | * |
556 | * @param bool $regenerate Should we regenerate token? (default false) |
557 | * @param int $maxTokens The maximum number of tokens to store in the |
558 | * session. |
559 | * |
560 | * @return string |
561 | */ |
562 | public function getCsrfHash($regenerate = false, $maxTokens = 5) |
563 | { |
564 | // Reset token store if we've overflowed the limit: |
565 | $this->csrf->trimTokenList($maxTokens); |
566 | return $this->csrf->getHash($regenerate); |
567 | } |
568 | |
569 | /** |
570 | * Get the logged-in user's identity (null if not logged in) |
571 | * |
572 | * @return ?IdentityInterface |
573 | */ |
574 | public function getIdentity() |
575 | { |
576 | return $this->getUserObject(); |
577 | } |
578 | |
579 | /** |
580 | * Resets the session if the logged in user's credentials have expired. |
581 | * |
582 | * @return bool True if session has expired. |
583 | */ |
584 | public function checkForExpiredCredentials() |
585 | { |
586 | if ($this->getIdentity() && $this->getAuth()->isExpired()) { |
587 | $this->logout(null, false); |
588 | return true; |
589 | } |
590 | return false; |
591 | } |
592 | |
593 | /** |
594 | * Are we in privacy mode? |
595 | * |
596 | * @return bool |
597 | */ |
598 | public function inPrivacyMode() |
599 | { |
600 | return $this->config->Authentication->privacy ?? false; |
601 | } |
602 | |
603 | /** |
604 | * Updates the user information in the session. |
605 | * |
606 | * @param UserEntityInterface $user User object to store in the session |
607 | * |
608 | * @return void |
609 | */ |
610 | public function updateSession($user) |
611 | { |
612 | $this->currentUser = $user; |
613 | if ($this->inPrivacyMode()) { |
614 | $this->userSession->addUserDataToSession($user); |
615 | } else { |
616 | $this->userSession->addUserIdToSession($user->getId()); |
617 | } |
618 | $this->cookieManager->clear('loggedOut'); |
619 | } |
620 | |
621 | /** |
622 | * Create a new user account from the request. |
623 | * |
624 | * @param \Laminas\Http\PhpEnvironment\Request $request Request object containing |
625 | * new account details. |
626 | * |
627 | * @throws AuthException |
628 | * @return UserEntityInterface New user entity. |
629 | */ |
630 | public function create($request) |
631 | { |
632 | $user = $this->getAuth()->create($request); |
633 | $this->updateUser($user, $this->getSelectedAuthMethod()); |
634 | $this->updateSession($user); |
635 | return $user; |
636 | } |
637 | |
638 | /** |
639 | * Update a user's password from the request. |
640 | * |
641 | * @param \Laminas\Http\PhpEnvironment\Request $request Request object containing |
642 | * password change details. |
643 | * |
644 | * @throws AuthException |
645 | * @return UserEntityInterface Updated user entity. |
646 | */ |
647 | public function updatePassword($request) |
648 | { |
649 | $user = $this->getAuth()->updatePassword($request); |
650 | $this->updateSession($user); |
651 | return $user; |
652 | } |
653 | |
654 | /** |
655 | * Update a user's email from the request. |
656 | * |
657 | * @param UserEntityInterface $user Object representing user being updated. |
658 | * @param string $email New email address to set (must be pre-validated!). |
659 | * |
660 | * @throws AuthException |
661 | * @return void |
662 | * |
663 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
664 | */ |
665 | public function updateEmail(UserEntityInterface $user, $email) |
666 | { |
667 | // Depending on verification setting, either do a direct update or else |
668 | // put the new address into a pending state. |
669 | if ($this->config->Authentication->verify_email ?? false) { |
670 | // If new email address is the current address, just reset any pending |
671 | // email address: |
672 | $user->setPendingEmail($email === $user->getEmail() ? '' : $email); |
673 | } else { |
674 | $this->userService->updateUserEmail($user, $email, true); |
675 | $user->setPendingEmail(''); |
676 | } |
677 | $this->userService->persistEntity($user); |
678 | $this->updateSession($user); |
679 | } |
680 | |
681 | /** |
682 | * Update the verification hash for the provided user. |
683 | * |
684 | * @param UserEntityInterface $user User to update |
685 | * |
686 | * @return void |
687 | */ |
688 | public function updateUserVerifyHash(UserEntityInterface $user): void |
689 | { |
690 | $hash = md5($user->getUsername() . $user->getRawCatPassword() . $user->getPasswordHash() . rand()); |
691 | // Make totally sure the timestamp is exactly 10 characters: |
692 | $time = str_pad(substr((string)time(), 0, 10), 10, '0', STR_PAD_LEFT); |
693 | $user->setVerifyHash($hash . $time); |
694 | $this->userService->persistEntity($user); |
695 | } |
696 | |
697 | /** |
698 | * Try to log in the user using current query parameters; return User object |
699 | * on success, throws exception on failure. |
700 | * |
701 | * @param \Laminas\Http\PhpEnvironment\Request $request Request object containing |
702 | * account credentials. |
703 | * |
704 | * @throws AuthException |
705 | * @throws \VuFind\Exception\PasswordSecurity |
706 | * @throws \VuFind\Exception\AuthInProgress |
707 | * @return UserEntityInterface Object representing logged-in user. |
708 | */ |
709 | public function login($request) |
710 | { |
711 | // Wrap everything in try-catch so that we can reset the state on failure: |
712 | try { |
713 | // Allow the auth module to inspect the request (used by ChoiceAuth, |
714 | // for example): |
715 | $this->getAuth()->preLoginCheck($request); |
716 | |
717 | // Get the main auth method before switching to any delegate: |
718 | $mainAuthMethod = $this->getSelectedAuthMethod(); |
719 | |
720 | // Check if the current auth method wants to delegate the request to another |
721 | // method: |
722 | if ($delegate = $this->getAuth()->getDelegateAuthMethod($request)) { |
723 | $this->setAuthMethod($delegate, true); |
724 | } |
725 | |
726 | // Validate CSRF for form-based authentication methods: |
727 | if ( |
728 | !$this->getAuth()->getSessionInitiator('') |
729 | && $this->getAuth()->needsCsrfCheck($request) |
730 | ) { |
731 | if (!$this->csrf->isValid($request->getPost()->get('csrf'))) { |
732 | $this->getAuth()->resetState(); |
733 | $this->logWarning('Invalid CSRF token passed to login'); |
734 | throw new AuthException('authentication_error_technical'); |
735 | } else { |
736 | // After successful token verification, clear list to shrink session: |
737 | $this->csrf->trimTokenList(0); |
738 | } |
739 | } |
740 | |
741 | // Perform authentication: |
742 | try { |
743 | $user = $this->getAuth()->authenticate($request); |
744 | } catch (AuthException $e) { |
745 | // Pass authentication exceptions through unmodified |
746 | throw $e; |
747 | } catch (\VuFind\Exception\PasswordSecurity $e) { |
748 | // Pass password security exceptions through unmodified |
749 | throw $e; |
750 | } catch (\Exception $e) { |
751 | // Catch other exceptions, log verbosely, and treat them as technical |
752 | // difficulties |
753 | $this->logError((string)$e); |
754 | throw new AuthException('authentication_error_technical', 0, $e); |
755 | } |
756 | |
757 | // Update user object |
758 | $this->updateUser($user, $mainAuthMethod); |
759 | |
760 | if ($request->getPost()->get('remember_me') && $this->supportsPersistentLogin($mainAuthMethod)) { |
761 | try { |
762 | $this->loginTokenManager->createToken($user, $this->sessionManager->getId()); |
763 | } catch (\Exception $e) { |
764 | $this->logError((string)$e); |
765 | throw new AuthException('authentication_error_technical', 0, $e); |
766 | } |
767 | } |
768 | // Store the user in the session and send it back to the caller: |
769 | $this->updateSession($user); |
770 | return $user; |
771 | } catch (\Exception $e) { |
772 | $this->getAuth()->resetState(); |
773 | throw $e; |
774 | } |
775 | } |
776 | |
777 | /** |
778 | * Delete a login token |
779 | * |
780 | * @param string $series Series to identify the token |
781 | * |
782 | * @return void |
783 | */ |
784 | public function deleteToken(string $series) |
785 | { |
786 | $this->loginTokenManager->deleteTokenSeries($series); |
787 | } |
788 | |
789 | /** |
790 | * Delete all login tokens for a user |
791 | * |
792 | * @param int $userId User identifier |
793 | * |
794 | * @return void |
795 | */ |
796 | public function deleteUserLoginTokens(int $userId) |
797 | { |
798 | $this->loginTokenManager->deleteUserLoginTokens($userId); |
799 | } |
800 | |
801 | /** |
802 | * Setter |
803 | * |
804 | * @param string $method The auth class to proxy |
805 | * @param bool $forceLegal Whether to force the new method legal |
806 | * |
807 | * @return void |
808 | */ |
809 | public function setAuthMethod($method, $forceLegal = false) |
810 | { |
811 | // Change the setting: |
812 | $this->activeAuth = $method; |
813 | |
814 | if ($forceLegal) { |
815 | if (!in_array($method, $this->legalAuthOptions)) { |
816 | $this->legalAuthOptions[] = $method; |
817 | } |
818 | } |
819 | |
820 | // If this method supports switching to a different method and we haven't |
821 | // already initialized it, add those options to the legal list. If the object |
822 | // is already initialized, that means we've already gone through this step |
823 | // and can save ourselves the trouble. |
824 | |
825 | // This code also has the side effect of validating $method, since if an |
826 | // invalid value was passed in, the call to getSelectableAuthOptions will |
827 | // throw an exception. |
828 | if (!isset($this->auth[$method])) { |
829 | $this->legalAuthOptions = array_unique( |
830 | array_merge( |
831 | $this->legalAuthOptions, |
832 | $this->getSelectableAuthOptions() |
833 | ) |
834 | ); |
835 | } |
836 | } |
837 | |
838 | /** |
839 | * Validate the credentials in the provided request, but do not change the state |
840 | * of the current logged-in user. Return true for valid credentials, false |
841 | * otherwise. |
842 | * |
843 | * @param \Laminas\Http\PhpEnvironment\Request $request Request object containing |
844 | * account credentials. |
845 | * |
846 | * @throws AuthException |
847 | * @return bool |
848 | */ |
849 | public function validateCredentials($request) |
850 | { |
851 | return $this->getAuth()->validateCredentials($request); |
852 | } |
853 | |
854 | /** |
855 | * What login method does the ILS use (password, email, vufind) |
856 | * |
857 | * @param string $target Login target (MultiILS only) |
858 | * |
859 | * @return array|false |
860 | */ |
861 | public function getILSLoginMethod($target = '') |
862 | { |
863 | $auth = $this->getAuth(); |
864 | if (is_callable([$auth, 'getILSLoginMethod'])) { |
865 | return $auth->getILSLoginMethod($target); |
866 | } |
867 | return false; |
868 | } |
869 | |
870 | /** |
871 | * Connect authenticated user as library card to his account. |
872 | * |
873 | * @param \Laminas\Http\PhpEnvironment\Request $request Request object |
874 | * containing account credentials. |
875 | * @param UserEntityInterface $user Connect newly created |
876 | * library card to this user. |
877 | * |
878 | * @return void |
879 | * @throws \Exception |
880 | */ |
881 | public function connectLibraryCard($request, $user) |
882 | { |
883 | $auth = $this->getAuth(); |
884 | if (!$auth->supportsConnectingLibraryCard()) { |
885 | throw new \Exception('Connecting of library cards is not supported'); |
886 | } |
887 | $auth->connectLibraryCard($request, $user); |
888 | } |
889 | |
890 | /** |
891 | * Update common user attributes on login |
892 | * |
893 | * @param UserEntityInterface $user User object |
894 | * @param ?string $authMethod Authentication method to user |
895 | * |
896 | * @return void |
897 | */ |
898 | protected function updateUser($user, $authMethod) |
899 | { |
900 | if ($authMethod) { |
901 | $user->setAuthMethod(strtolower($authMethod)); |
902 | } |
903 | $user->setLastLogin(new \DateTime()); |
904 | $this->userService->persistEntity($user); |
905 | } |
906 | |
907 | /** |
908 | * Is the user allowed to log directly into the ILS? |
909 | * |
910 | * @return bool |
911 | */ |
912 | public function allowsUserIlsLogin(): bool |
913 | { |
914 | return $this->config->Catalog->allowUserLogin ?? true; |
915 | } |
916 | |
917 | /** |
918 | * Process a raw policy configuration |
919 | * |
920 | * @param array $policy Policy configuration |
921 | * |
922 | * @return array |
923 | */ |
924 | protected function processPolicyConfig(array $policy): array |
925 | { |
926 | // Convert 'numeric' or 'alphanumeric' pattern to a regular expression: |
927 | switch ($policy['pattern'] ?? '') { |
928 | case 'numeric': |
929 | $policy['pattern'] = '\d+'; |
930 | break; |
931 | case 'alphanumeric': |
932 | $policy['pattern'] = '[\da-zA-Z]+'; |
933 | } |
934 | |
935 | // Map settings to attributes for a text input field: |
936 | $inputMap = [ |
937 | 'minLength' => 'data-minlength', |
938 | 'maxLength' => 'maxlength', |
939 | 'pattern' => 'pattern', |
940 | ]; |
941 | $policy['inputAttrs'] = []; |
942 | foreach ($inputMap as $from => $to) { |
943 | if (isset($policy[$from])) { |
944 | $policy['inputAttrs'][$to] = $policy[$from]; |
945 | } |
946 | } |
947 | return $policy; |
948 | } |
949 | } |