Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
50.49% |
52 / 103 |
|
56.25% |
9 / 16 |
CRAP | |
0.00% |
0 / 1 |
ILSAuthenticator | |
50.49% |
52 / 103 |
|
56.25% |
9 / 16 |
256.14 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
passwordEncryptionEnabled | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
decrypt | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
encrypt | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
encryptOrDecrypt | |
42.31% |
11 / 26 |
|
0.00% |
0 / 1 |
24.55 | |||
getCatPasswordForUser | |
28.57% |
2 / 7 |
|
0.00% |
0 / 1 |
9.83 | |||
setUserCatalogCredentials | |
66.67% |
4 / 6 |
|
0.00% |
0 / 1 |
2.15 | |||
saveUserCatalogCredentials | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
updateUserHomeLibrary | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
getStoredCatalogCredentials | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
storedCatalogLogin | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
5 | |||
newCatalogLogin | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
sendEmailLoginLink | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
12 | |||
processEmailLoginHash | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
updateUser | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
getAuthManager | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 |
1 | <?php |
2 | |
3 | /** |
4 | * Class for managing ILS-specific authentication. |
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\Crypt\BlockCipher; |
34 | use Laminas\Crypt\Symmetric\Openssl; |
35 | use VuFind\Db\Entity\UserEntityInterface; |
36 | use VuFind\Db\Service\DbServiceAwareInterface; |
37 | use VuFind\Db\Service\DbServiceAwareTrait; |
38 | use VuFind\Db\Service\UserCardServiceInterface; |
39 | use VuFind\Db\Service\UserServiceInterface; |
40 | use VuFind\Exception\ILS as ILSException; |
41 | use VuFind\ILS\Connection as ILSConnection; |
42 | |
43 | /** |
44 | * Class for managing ILS-specific authentication. |
45 | * |
46 | * @category VuFind |
47 | * @package Authentication |
48 | * @author Demian Katz <demian.katz@villanova.edu> |
49 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
50 | * @link https://vufind.org Main Page |
51 | */ |
52 | class ILSAuthenticator implements DbServiceAwareInterface |
53 | { |
54 | use DbServiceAwareTrait; |
55 | |
56 | /** |
57 | * Callback for retrieving the authentication manager |
58 | * |
59 | * @var callable |
60 | */ |
61 | protected $authManagerCallback; |
62 | |
63 | /** |
64 | * Authentication manager |
65 | * |
66 | * @var Manager |
67 | */ |
68 | protected $authManager = null; |
69 | |
70 | /** |
71 | * Cache for ILS account information (keyed by username) |
72 | * |
73 | * @var array |
74 | */ |
75 | protected $ilsAccount = []; |
76 | |
77 | /** |
78 | * Is encryption enabled? |
79 | * |
80 | * @var bool |
81 | */ |
82 | protected $encryptionEnabled = null; |
83 | |
84 | /** |
85 | * Encryption key used for catalog passwords (null if encryption disabled): |
86 | * |
87 | * @var string |
88 | */ |
89 | protected $encryptionKey = null; |
90 | |
91 | /** |
92 | * Constructor |
93 | * |
94 | * @param callable $authCB Auth manager callback |
95 | * @param ILSConnection $catalog ILS connection |
96 | * @param ?EmailAuthenticator $emailAuthenticator Email authenticator |
97 | * @param ?Config $config Configuration from config.ini |
98 | */ |
99 | public function __construct( |
100 | callable $authCB, |
101 | protected ILSConnection $catalog, |
102 | protected ?EmailAuthenticator $emailAuthenticator = null, |
103 | protected ?Config $config = null |
104 | ) { |
105 | $this->authManagerCallback = $authCB; |
106 | } |
107 | |
108 | /** |
109 | * Is ILS password encryption enabled? |
110 | * |
111 | * @return bool |
112 | */ |
113 | public function passwordEncryptionEnabled() |
114 | { |
115 | if (null === $this->encryptionEnabled) { |
116 | $this->encryptionEnabled |
117 | = $this->config->Authentication->encrypt_ils_password ?? false; |
118 | } |
119 | return $this->encryptionEnabled; |
120 | } |
121 | |
122 | /** |
123 | * Decrypt text. |
124 | * |
125 | * @param ?string $text The text to decrypt (null values will be returned as null) |
126 | * |
127 | * @return ?string|bool The decrypted string (null if empty or false if invalid) |
128 | * @throws \VuFind\Exception\PasswordSecurity |
129 | */ |
130 | public function decrypt(?string $text) |
131 | { |
132 | return $this->encryptOrDecrypt($text, false); |
133 | } |
134 | |
135 | /** |
136 | * Encrypt text. |
137 | * |
138 | * @param ?string $text The text to encrypt (null values will be returned as null) |
139 | * |
140 | * @return ?string|bool The encrypted string (null if empty or false if invalid) |
141 | * @throws \VuFind\Exception\PasswordSecurity |
142 | */ |
143 | public function encrypt(?string $text) |
144 | { |
145 | return $this->encryptOrDecrypt($text, true); |
146 | } |
147 | |
148 | /** |
149 | * This is a central function for encrypting and decrypting so that |
150 | * logic is all in one location |
151 | * |
152 | * @param ?string $text The text to be encrypted or decrypted |
153 | * @param bool $encrypt True if we wish to encrypt text, False if we wish to |
154 | * decrypt text. |
155 | * |
156 | * @return ?string|bool The encrypted/decrypted string (null = empty input; false = error) |
157 | * @throws \VuFind\Exception\PasswordSecurity |
158 | */ |
159 | protected function encryptOrDecrypt(?string $text, bool $encrypt = true) |
160 | { |
161 | // Ignore empty text: |
162 | if ($text === null || $text === '') { |
163 | return null; |
164 | } |
165 | |
166 | $configAuth = $this->config->Authentication ?? new \Laminas\Config\Config([]); |
167 | |
168 | // Load encryption key from configuration if not already present: |
169 | if ($this->encryptionKey === null) { |
170 | if (empty($configAuth->ils_encryption_key)) { |
171 | throw new \VuFind\Exception\PasswordSecurity( |
172 | 'ILS password encryption on, but no key set.' |
173 | ); |
174 | } |
175 | |
176 | $this->encryptionKey = $configAuth->ils_encryption_key; |
177 | } |
178 | |
179 | // Perform encryption: |
180 | $algo = $configAuth->ils_encryption_algo ?? 'blowfish'; |
181 | |
182 | // Check if OpenSSL error is caused by blowfish support |
183 | try { |
184 | $cipher = new BlockCipher(new Openssl(['algorithm' => $algo])); |
185 | if ($algo == 'blowfish') { |
186 | trigger_error( |
187 | 'Deprecated encryption algorithm (blowfish) detected', |
188 | E_USER_DEPRECATED |
189 | ); |
190 | } |
191 | } catch (\InvalidArgumentException $e) { |
192 | if ($algo == 'blowfish') { |
193 | throw new \VuFind\Exception\PasswordSecurity( |
194 | 'The blowfish encryption algorithm ' . |
195 | 'is not supported by your version of OpenSSL. ' . |
196 | 'Please visit /Upgrade/CriticalFixBlowfish for further details.' |
197 | ); |
198 | } else { |
199 | throw $e; |
200 | } |
201 | } |
202 | $cipher->setKey($this->encryptionKey); |
203 | return $encrypt ? $cipher->encrypt($text) : $cipher->decrypt($text); |
204 | } |
205 | |
206 | /** |
207 | * Given a user object, retrieve the decrypted password (or null if unset/invalid). |
208 | * |
209 | * @param UserEntityInterface $user User |
210 | * |
211 | * @return ?string |
212 | */ |
213 | public function getCatPasswordForUser(UserEntityInterface $user) |
214 | { |
215 | if ($this->passwordEncryptionEnabled()) { |
216 | $encrypted = $user->getCatPassEnc(); |
217 | $decrypted = !empty($encrypted) ? $this->decrypt($encrypted) : null; |
218 | if ($decrypted === false) { |
219 | // Unexpected error decrypting password; let's treat it as unset for now: |
220 | return null; |
221 | } |
222 | return $decrypted; |
223 | } |
224 | return $user->getRawCatPassword(); |
225 | } |
226 | |
227 | /** |
228 | * Set ILS login credentials for a user without saving them. |
229 | * |
230 | * @param UserEntityInterface $user User to update |
231 | * @param string $username Username to save |
232 | * @param ?string $password Password to save (null for none) |
233 | * |
234 | * @return void |
235 | */ |
236 | public function setUserCatalogCredentials(UserEntityInterface $user, string $username, ?string $password): void |
237 | { |
238 | $user->setCatUsername($username); |
239 | if ($this->passwordEncryptionEnabled()) { |
240 | $user->setRawCatPassword(null); |
241 | $user->setCatPassEnc($this->encrypt($password)); |
242 | } else { |
243 | $user->setRawCatPassword($password); |
244 | $user->setCatPassEnc(null); |
245 | } |
246 | } |
247 | |
248 | /** |
249 | * Save ILS login credentials. |
250 | * |
251 | * @param UserEntityInterface $user User to update |
252 | * @param string $username Username to save |
253 | * @param ?string $password Password to save |
254 | * |
255 | * @return void |
256 | * @throws \VuFind\Exception\PasswordSecurity |
257 | */ |
258 | public function saveUserCatalogCredentials(UserEntityInterface $user, string $username, ?string $password): void |
259 | { |
260 | $this->setUserCatalogCredentials($user, $username, $password); |
261 | $this->getDbService(UserServiceInterface::class)->persistEntity($user); |
262 | |
263 | // Update library card entry after saving the user so that we always have a |
264 | // user id: |
265 | $this->getDbService(UserCardServiceInterface::class)->synchronizeUserLibraryCardData($user); |
266 | } |
267 | |
268 | /** |
269 | * Change and persist the user's home library. |
270 | * |
271 | * @param UserEntityInterface $user User to update |
272 | * @param ?string $homeLibrary New home library value (or null to clear) |
273 | * |
274 | * @return void |
275 | */ |
276 | public function updateUserHomeLibrary(UserEntityInterface $user, ?string $homeLibrary): void |
277 | { |
278 | // Update the home library and make sure library cards are kept in sync: |
279 | $user->setHomeLibrary($homeLibrary); |
280 | $this->getDbService(UserCardServiceInterface::class)->synchronizeUserLibraryCardData($user); |
281 | $this->getDbService(UserServiceInterface::class)->persistEntity($user); |
282 | $this->getAuthManager()->updateSession($user); |
283 | } |
284 | |
285 | /** |
286 | * Get stored catalog credentials for the current user. |
287 | * |
288 | * Returns associative array of cat_username and cat_password if they are |
289 | * available, false otherwise. This method does not verify that the credentials |
290 | * are valid. |
291 | * |
292 | * @return array|bool |
293 | */ |
294 | public function getStoredCatalogCredentials() |
295 | { |
296 | // Fail if no username is found, but allow a missing password (not every ILS |
297 | // requires a password to connect). |
298 | if (($user = $this->getAuthManager()->getUserObject()) && ($username = $user->getCatUsername())) { |
299 | return [ |
300 | 'cat_username' => $username, |
301 | 'cat_password' => $this->getCatPasswordForUser($user), |
302 | ]; |
303 | } |
304 | return false; |
305 | } |
306 | |
307 | /** |
308 | * Log the current user into the catalog using stored credentials; if this |
309 | * fails, clear the user's stored credentials so they can enter new, corrected |
310 | * ones. |
311 | * |
312 | * Returns associative array of patron data on success, false on failure. |
313 | * |
314 | * @return array|bool |
315 | */ |
316 | public function storedCatalogLogin() |
317 | { |
318 | // Fail if no username is found, but allow a missing password (not every ILS |
319 | // requires a password to connect). |
320 | if (($user = $this->getAuthManager()->getUserObject()) && ($username = $user->getCatUsername())) { |
321 | // Do we have a previously cached ILS account? |
322 | if (isset($this->ilsAccount[$username])) { |
323 | return $this->ilsAccount[$username]; |
324 | } |
325 | $patron = $this->catalog->patronLogin( |
326 | $username, |
327 | $this->getCatPasswordForUser($user) |
328 | ); |
329 | if (empty($patron)) { |
330 | // Problem logging in -- clear user credentials so they can be |
331 | // prompted again; perhaps their password has changed in the |
332 | // system! |
333 | $user->setCatUsername(null)->setRawCatPassword(null)->setCatPassEnc(null); |
334 | } else { |
335 | // cache for future use |
336 | $this->ilsAccount[$username] = $patron; |
337 | return $patron; |
338 | } |
339 | } |
340 | |
341 | return false; |
342 | } |
343 | |
344 | /** |
345 | * Attempt to log in the user to the ILS, and save credentials if it works. |
346 | * |
347 | * @param string $username Catalog username |
348 | * @param string $password Catalog password |
349 | * |
350 | * Returns associative array of patron data on success, false on failure. |
351 | * |
352 | * @return array|bool |
353 | * @throws ILSException |
354 | */ |
355 | public function newCatalogLogin($username, $password) |
356 | { |
357 | $result = $this->catalog->patronLogin($username, $password); |
358 | if ($result) { |
359 | $this->updateUser($username, $password, $result); |
360 | return $result; |
361 | } |
362 | return false; |
363 | } |
364 | |
365 | /** |
366 | * Send email authentication link |
367 | * |
368 | * @param string $email Email address |
369 | * @param string $route Route for the login link |
370 | * @param array $routeParams Route parameters |
371 | * @param array $urlParams URL parameters |
372 | * |
373 | * @return void |
374 | */ |
375 | public function sendEmailLoginLink($email, $route, $routeParams = [], $urlParams = []) |
376 | { |
377 | if (null === $this->emailAuthenticator) { |
378 | throw new \Exception('Email authenticator not set'); |
379 | } |
380 | |
381 | $userData = $this->catalog->patronLogin($email, ''); |
382 | if ($userData) { |
383 | $this->emailAuthenticator->sendAuthenticationLink( |
384 | $userData['email'], |
385 | compact('userData'), |
386 | ['auth_method' => 'ILS'] + $urlParams, |
387 | $route, |
388 | $routeParams |
389 | ); |
390 | } |
391 | } |
392 | |
393 | /** |
394 | * Process email login |
395 | * |
396 | * @param string $hash Login hash |
397 | * |
398 | * @return array|bool |
399 | * @throws ILSException |
400 | */ |
401 | public function processEmailLoginHash($hash) |
402 | { |
403 | if (null === $this->emailAuthenticator) { |
404 | throw new \Exception('Email authenticator not set'); |
405 | } |
406 | |
407 | try { |
408 | $loginData = $this->emailAuthenticator->authenticate($hash); |
409 | // Check if we have more granular data available: |
410 | $patron = $loginData['userData'] ?? $loginData; |
411 | } catch (\VuFind\Exception\Auth $e) { |
412 | return false; |
413 | } |
414 | $this->updateUser($patron['cat_username'], '', $patron); |
415 | return $patron; |
416 | } |
417 | |
418 | /** |
419 | * Update current user account with the patron information |
420 | * |
421 | * @param string $catUsername Catalog username |
422 | * @param string $catPassword Catalog password |
423 | * @param array $patron Patron |
424 | * |
425 | * @return void |
426 | */ |
427 | protected function updateUser($catUsername, $catPassword, $patron) |
428 | { |
429 | $user = $this->getAuthManager()->getUserObject(); |
430 | if ($user) { |
431 | $this->saveUserCatalogCredentials($user, $catUsername, $catPassword); |
432 | $this->getAuthManager()->updateSession($user); |
433 | // cache for future use |
434 | $this->ilsAccount[$catUsername] = $patron; |
435 | } |
436 | } |
437 | |
438 | /** |
439 | * Get authentication manager |
440 | * |
441 | * @return Manager |
442 | */ |
443 | protected function getAuthManager(): Manager |
444 | { |
445 | if (null === $this->authManager) { |
446 | $this->authManager = ($this->authManagerCallback)(); |
447 | } |
448 | return $this->authManager; |
449 | } |
450 | } |