Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
80.37% |
86 / 107 |
|
48.15% |
13 / 27 |
CRAP | |
0.00% |
0 / 1 |
AbstractBase | |
80.37% |
86 / 107 |
|
48.15% |
13 / 27 |
68.90 | |
0.00% |
0 / 1 |
getConfig | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
preLoginCheck | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
resetState | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setConfig | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
needsCsrfCheck | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDelegateAuthMethod | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
validateConfig | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
authenticate | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
validateCredentials | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
isExpired | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
create | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
updatePassword | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getSessionInitiator | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
logout | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
supportsCreation | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
supportsPasswordChange | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
supportsPasswordRecovery | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
supportsConnectingLibraryCard | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getCannedPolicyHint | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
getPolicyConfig | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
5 | |||
getUsernamePolicy | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getPasswordPolicy | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getUserService | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
validateUsernameAgainstPolicy | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
validatePasswordAgainstPolicy | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
validateStringAgainstPolicy | |
100.00% |
37 / 37 |
|
100.00% |
1 / 1 |
14 | |||
getOrCreateUserByUsername | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
setUserValueByField | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
3.14 |
1 | <?php |
2 | |
3 | /** |
4 | * Abstract authentication base class |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * Copyright (C) Villanova University 2010. |
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 Franck Borel <franck.borel@gbv.de> |
26 | * @author Demian Katz <demian.katz@villanova.edu> |
27 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
28 | * @link https://vufind.org Main Page |
29 | */ |
30 | |
31 | namespace VuFind\Auth; |
32 | |
33 | use Exception; |
34 | use Laminas\Http\PhpEnvironment\Request; |
35 | use VuFind\Db\Entity\UserEntityInterface; |
36 | use VuFind\Db\Service\UserServiceInterface; |
37 | use VuFind\Exception\Auth as AuthException; |
38 | |
39 | use function get_class; |
40 | use function in_array; |
41 | use function is_callable; |
42 | |
43 | /** |
44 | * Abstract authentication base class |
45 | * |
46 | * @category VuFind |
47 | * @package Authentication |
48 | * @author Franck Borel <franck.borel@gbv.de> |
49 | * @author Demian Katz <demian.katz@villanova.edu> |
50 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
51 | * @link https://vufind.org Main Page |
52 | */ |
53 | abstract class AbstractBase implements |
54 | \VuFind\Db\Service\DbServiceAwareInterface, |
55 | \VuFind\I18n\Translator\TranslatorAwareInterface, |
56 | \Laminas\Log\LoggerAwareInterface |
57 | { |
58 | use \VuFind\Db\Service\DbServiceAwareTrait; |
59 | use \VuFind\I18n\Translator\TranslatorAwareTrait; |
60 | use \VuFind\Log\LoggerAwareTrait; |
61 | |
62 | /** |
63 | * Has the configuration been validated? |
64 | * |
65 | * @var bool |
66 | */ |
67 | protected $configValidated = false; |
68 | |
69 | /** |
70 | * Configuration settings |
71 | * |
72 | * @var \Laminas\Config\Config |
73 | */ |
74 | protected $config = null; |
75 | |
76 | /** |
77 | * Map of database column name to setter method for UserEntityInterface objects. |
78 | * |
79 | * @return array |
80 | */ |
81 | protected $userSetterMap = [ |
82 | 'cat_username' => 'setCatUsername', |
83 | 'college' => 'setCollege', |
84 | 'email' => 'setEmail', |
85 | 'firstname' => 'setFirstname', |
86 | 'lastname' => 'setLastname', |
87 | 'home_library' => 'setHomeLibrary', |
88 | 'major' => 'setMajor', |
89 | ]; |
90 | |
91 | /** |
92 | * Get configuration (load automatically if not previously set). Throw an |
93 | * exception if the configuration is invalid. |
94 | * |
95 | * @throws AuthException |
96 | * @return \Laminas\Config\Config |
97 | */ |
98 | public function getConfig() |
99 | { |
100 | // Validate configuration if not already validated: |
101 | if (!$this->configValidated) { |
102 | $this->validateConfig(); |
103 | $this->configValidated = true; |
104 | } |
105 | |
106 | return $this->config; |
107 | } |
108 | |
109 | /** |
110 | * Inspect the user's request prior to processing a login request; this is |
111 | * essentially an event hook which most auth modules can ignore. See |
112 | * ChoiceAuth for a use case example. |
113 | * |
114 | * @param Request $request Request object. |
115 | * |
116 | * @throws AuthException |
117 | * @return void |
118 | * |
119 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
120 | */ |
121 | public function preLoginCheck($request) |
122 | { |
123 | // By default, do no checking. |
124 | } |
125 | |
126 | /** |
127 | * Reset any internal status; this is essentially an event hook which most auth |
128 | * modules can ignore. See ChoiceAuth for a use case example. |
129 | * |
130 | * @return void |
131 | */ |
132 | public function resetState() |
133 | { |
134 | // By default, do no checking. |
135 | } |
136 | |
137 | /** |
138 | * Set configuration. |
139 | * |
140 | * @param \Laminas\Config\Config $config Configuration to set |
141 | * |
142 | * @return void |
143 | */ |
144 | public function setConfig($config) |
145 | { |
146 | $this->config = $config; |
147 | $this->configValidated = false; |
148 | } |
149 | |
150 | /** |
151 | * Whether this authentication method needs CSRF checking for the request. |
152 | * |
153 | * @param Request $request Request object. |
154 | * |
155 | * @return bool |
156 | * |
157 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
158 | */ |
159 | public function needsCsrfCheck($request) |
160 | { |
161 | // Enabled by default |
162 | return true; |
163 | } |
164 | |
165 | /** |
166 | * Returns any authentication method this request should be delegated to. |
167 | * |
168 | * @param Request $request Request object. |
169 | * |
170 | * @return string|bool |
171 | * |
172 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
173 | */ |
174 | public function getDelegateAuthMethod(Request $request) |
175 | { |
176 | // No delegate by default |
177 | return false; |
178 | } |
179 | |
180 | /** |
181 | * Validate configuration parameters. This is a support method for getConfig(), |
182 | * so the configuration MUST be accessed using $this->config; do not call |
183 | * $this->getConfig() from within this method! |
184 | * |
185 | * @throws AuthException |
186 | * @return void |
187 | */ |
188 | protected function validateConfig() |
189 | { |
190 | // By default, do no checking. |
191 | } |
192 | |
193 | /** |
194 | * Attempt to authenticate the current user. Throws exception if login fails. |
195 | * |
196 | * @param Request $request Request object containing account credentials. |
197 | * |
198 | * @throws AuthException |
199 | * @return UserEntityInterface Object representing logged-in user. |
200 | */ |
201 | abstract public function authenticate($request); |
202 | |
203 | /** |
204 | * Validate the credentials in the provided request, but do not change the state |
205 | * of the current logged-in user. Return true for valid credentials, false |
206 | * otherwise. |
207 | * |
208 | * @param Request $request Request object containing account credentials. |
209 | * |
210 | * @throws AuthException |
211 | * @return bool |
212 | */ |
213 | public function validateCredentials($request) |
214 | { |
215 | try { |
216 | $user = $this->authenticate($request); |
217 | } catch (AuthException $e) { |
218 | return false; |
219 | } |
220 | return $user instanceof UserEntityInterface; |
221 | } |
222 | |
223 | /** |
224 | * Has the user's login expired? |
225 | * |
226 | * @return bool |
227 | */ |
228 | public function isExpired() |
229 | { |
230 | // By default, logins do not expire: |
231 | return false; |
232 | } |
233 | |
234 | /** |
235 | * Create a new user account from the request. |
236 | * |
237 | * @param Request $request Request object containing new account details. |
238 | * |
239 | * @throws AuthException |
240 | * @return UserEntityInterface New user entity. |
241 | * |
242 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
243 | */ |
244 | public function create($request) |
245 | { |
246 | throw new AuthException( |
247 | 'Account creation not supported by ' . get_class($this) |
248 | ); |
249 | } |
250 | |
251 | /** |
252 | * Update a user's password from the request. |
253 | * |
254 | * @param Request $request Request object containing new account details. |
255 | * |
256 | * @throws AuthException |
257 | * @return UserEntityInterface Updated user entity. |
258 | * |
259 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
260 | */ |
261 | public function updatePassword($request) |
262 | { |
263 | throw new AuthException( |
264 | 'Account password updating not supported by ' . get_class($this) |
265 | ); |
266 | } |
267 | |
268 | /** |
269 | * Get the URL to establish a session (needed when the internal VuFind login |
270 | * form is inadequate). Returns false when no session initiator is needed. |
271 | * |
272 | * @param string $target Full URL where external authentication method should |
273 | * send user after login (some drivers may override this). |
274 | * |
275 | * @return bool|string |
276 | * |
277 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
278 | */ |
279 | public function getSessionInitiator($target) |
280 | { |
281 | return false; |
282 | } |
283 | |
284 | /** |
285 | * Perform cleanup at logout time. |
286 | * |
287 | * @param string $url URL to redirect user to after logging out. |
288 | * |
289 | * @return string Redirect URL (usually same as $url, but modified in |
290 | * some authentication modules). |
291 | */ |
292 | public function logout($url) |
293 | { |
294 | // No special cleanup or URL modification needed by default. |
295 | return $url; |
296 | } |
297 | |
298 | /** |
299 | * Does this authentication method support account creation? |
300 | * |
301 | * @return bool |
302 | */ |
303 | public function supportsCreation() |
304 | { |
305 | // By default, account creation is not supported. |
306 | return false; |
307 | } |
308 | |
309 | /** |
310 | * Does this authentication method support password changing |
311 | * |
312 | * @return bool |
313 | */ |
314 | public function supportsPasswordChange() |
315 | { |
316 | // By default, password changing is not supported. |
317 | return false; |
318 | } |
319 | |
320 | /** |
321 | * Does this authentication method support password recovery |
322 | * |
323 | * @return bool |
324 | */ |
325 | public function supportsPasswordRecovery() |
326 | { |
327 | // By default, password recovery is not supported. |
328 | return false; |
329 | } |
330 | |
331 | /** |
332 | * Does this authentication method support connecting library card of |
333 | * currently authenticated user? |
334 | * |
335 | * @return bool |
336 | */ |
337 | public function supportsConnectingLibraryCard() |
338 | { |
339 | return method_exists($this, 'connectLibraryCard'); |
340 | } |
341 | |
342 | /** |
343 | * Return a canned username or password policy hint when available |
344 | * |
345 | * @param string $type Policy type (password or username) |
346 | * @param ?string $pattern Current policy pattern |
347 | * |
348 | * @return ?string |
349 | */ |
350 | protected function getCannedPolicyHint(string $type, ?string $pattern): ?string |
351 | { |
352 | /* Return a value according to the policy and pattern type, e.g.: |
353 | * |
354 | * 'numeric' => password_only_numeric or username_only_numeric |
355 | * 'alphanumeric' => password_only_alphanumeric or username_only_alphanumeric |
356 | * others => null (any hint should be defined by the password_hint or |
357 | * username_hint setting) |
358 | */ |
359 | return (in_array($pattern, ['numeric', 'alphanumeric'])) |
360 | ? $type . '_only_' . $pattern : null; |
361 | } |
362 | |
363 | /** |
364 | * Get a policy configuration |
365 | * |
366 | * @param string $type Policy type (password or username) |
367 | * |
368 | * @return array |
369 | */ |
370 | public function getPolicyConfig(string $type): array |
371 | { |
372 | $policy = []; |
373 | $config = $this->getConfig(); |
374 | $authConfig = isset($config->Authentication) |
375 | ? $config->Authentication->toArray() |
376 | : []; |
377 | /* Map settings to the policy array, e.g.: |
378 | * |
379 | * password_minimum_length or username_minimum_length => minLength |
380 | * password_maximum_length or username_maximum_length => maxLength |
381 | * password_pattern or username_pattern => pattern |
382 | * password_hint or username_hint => hint |
383 | */ |
384 | $map = [ |
385 | "minimum_{$type}_length" => 'minLength', |
386 | "maximum_{$type}_length" => 'maxLength', |
387 | "{$type}_pattern" => 'pattern', |
388 | "{$type}_hint" => 'hint', |
389 | ]; |
390 | foreach ($map as $iniSetting => $returnKey) { |
391 | if (null !== ($value = $authConfig[$iniSetting] ?? null)) { |
392 | $policy[$returnKey] = $value; |
393 | } |
394 | } |
395 | if (!isset($policy['hint'])) { |
396 | $policy['hint'] = $this->getCannedPolicyHint( |
397 | $type, |
398 | $policy['pattern'] ?? null |
399 | ); |
400 | } |
401 | return $policy; |
402 | } |
403 | |
404 | /** |
405 | * Get username policy for a new account (e.g. minLength, maxLength) |
406 | * |
407 | * @return array |
408 | */ |
409 | public function getUsernamePolicy() |
410 | { |
411 | return $this->getPolicyConfig('username'); |
412 | } |
413 | |
414 | /** |
415 | * Get password policy for a new password (e.g. minLength, maxLength) |
416 | * |
417 | * @return array |
418 | */ |
419 | public function getPasswordPolicy() |
420 | { |
421 | return $this->getPolicyConfig('password'); |
422 | } |
423 | |
424 | /** |
425 | * Get access to the user table. |
426 | * |
427 | * @return UserServiceInterface |
428 | */ |
429 | public function getUserService(): UserServiceInterface |
430 | { |
431 | return $this->getDbService(UserServiceInterface::class); |
432 | } |
433 | |
434 | /** |
435 | * Verify that a username fulfills the username policy. Throws exception if |
436 | * the username is invalid. |
437 | * |
438 | * @param string $username Password to verify |
439 | * |
440 | * @return void |
441 | * @throws AuthException |
442 | */ |
443 | protected function validateUsernameAgainstPolicy(string $username): void |
444 | { |
445 | $this->validateStringAgainstPolicy( |
446 | 'username', |
447 | $this->getUsernamePolicy(), |
448 | $username |
449 | ); |
450 | } |
451 | |
452 | /** |
453 | * Verify that a password fulfills the password policy. Throws exception if |
454 | * the password is invalid. |
455 | * |
456 | * @param string $password Password to verify |
457 | * |
458 | * @return void |
459 | * @throws AuthException |
460 | */ |
461 | protected function validatePasswordAgainstPolicy(string $password): void |
462 | { |
463 | $this->validateStringAgainstPolicy( |
464 | 'password', |
465 | $this->getPasswordPolicy(), |
466 | $password |
467 | ); |
468 | } |
469 | |
470 | /** |
471 | * Verify that a username or password fulfills the given policy. Throws exception |
472 | * if the string is invalid. |
473 | * |
474 | * @param string $type Policy type (password or username) |
475 | * @param array $policy Policy configuration |
476 | * @param string $string String to verify |
477 | * |
478 | * @return void |
479 | * @throws AuthException |
480 | */ |
481 | protected function validateStringAgainstPolicy( |
482 | string $type, |
483 | array $policy, |
484 | string $string |
485 | ): void { |
486 | if ( |
487 | isset($policy['minLength']) |
488 | && mb_strlen($string, 'UTF-8') < $policy['minLength'] |
489 | ) { |
490 | // e.g. password_minimum_length or username_minimum_length: |
491 | throw new AuthException( |
492 | $this->translate( |
493 | "{$type}_minimum_length", |
494 | ['%%minlength%%' => $policy['minLength']] |
495 | ) |
496 | ); |
497 | } |
498 | if ( |
499 | isset($policy['maxLength']) |
500 | && mb_strlen($string, 'UTF-8') > $policy['maxLength'] |
501 | ) { |
502 | // e.g. password_maximum_length or username_maximum_length: |
503 | throw new AuthException( |
504 | $this->translate( |
505 | "{$type}_maximum_length", |
506 | ['%%maxlength%%' => $policy['maxLength']] |
507 | ) |
508 | ); |
509 | } |
510 | if (!empty($policy['pattern'])) { |
511 | $valid = true; |
512 | if ($policy['pattern'] == 'numeric') { |
513 | if (!ctype_digit($string)) { |
514 | $valid = false; |
515 | } |
516 | } elseif ($policy['pattern'] == 'alphanumeric') { |
517 | if (preg_match('/[^\da-zA-Z]/', $string)) { |
518 | $valid = false; |
519 | } |
520 | } else { |
521 | $result = @preg_match( |
522 | "/({$policy['pattern']})/u", |
523 | $string, |
524 | $matches |
525 | ); |
526 | if ($result === false) { |
527 | throw new \Exception( |
528 | "Invalid regexp in $type pattern: " . $policy['pattern'] |
529 | ); |
530 | } |
531 | if (!$result || $matches[1] != $string) { |
532 | $valid = false; |
533 | } |
534 | } |
535 | if (!$valid) { |
536 | // e.g. password_error_invalid or username_error_invalid: |
537 | throw new AuthException($this->translate("{$type}_error_invalid")); |
538 | } |
539 | } |
540 | } |
541 | |
542 | /** |
543 | * Look up a user by username; create a new entity if no match is found. |
544 | * |
545 | * @param string $username Username |
546 | * |
547 | * @return UserEntityInterface |
548 | * @throws Exception |
549 | */ |
550 | protected function getOrCreateUserByUsername(string $username): UserEntityInterface |
551 | { |
552 | $userService = $this->getUserService(); |
553 | $user = $userService->getUserByUsername($username); |
554 | return $user ? $user : $userService->createEntityForUsername($username); |
555 | } |
556 | |
557 | /** |
558 | * Set a value in a UserEntityObject using a field name. |
559 | * |
560 | * @param UserEntityInterface $user User to update |
561 | * @param string $field Field name being updated |
562 | * @param mixed $value New value to set |
563 | * |
564 | * @return void |
565 | * @throws Exception |
566 | */ |
567 | protected function setUserValueByField(UserEntityInterface $user, string $field, $value): void |
568 | { |
569 | $setter = $this->userSetterMap[$field] ?? null; |
570 | if (!$setter || !is_callable([$user, $setter])) { |
571 | throw new Exception("Unsupported field: $field"); |
572 | } |
573 | $user->$setter($value); |
574 | } |
575 | } |