Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
72.73% |
56 / 77 |
|
53.85% |
14 / 26 |
CRAP | |
0.00% |
0 / 1 |
ChoiceAuth | |
72.73% |
56 / 77 |
|
53.85% |
14 / 26 |
83.27 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
validateConfig | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
setConfig | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
preLoginCheck | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
resetState | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
authenticate | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
create | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setPluginManager | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getPluginManager | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getSelectableAuthOptions | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getSelectedAuthOption | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
logout | |
57.14% |
4 / 7 |
|
0.00% |
0 / 1 |
5.26 | |||
getSessionInitiator | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
supportsPasswordChange | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
supportsPasswordRecovery | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getUsernamePolicy | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getPasswordPolicy | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
updatePassword | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getDelegateAuthMethod | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
hasLegalStrategy | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
proxyAuthMethod | |
77.78% |
7 / 9 |
|
0.00% |
0 / 1 |
4.18 | |||
proxyUserLoad | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
2.02 | |||
setStrategyFromRequest | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
5 | |||
setStrategy | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
validateCredentials | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
needsCsrfCheck | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | /** |
4 | * MultiAuth Authentication plugin |
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 Anna Headley <vufind-tech@lists.sourceforge.net> |
26 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
27 | * @link https://vufind.org/wiki/development:plugins:authentication_handlers Wiki |
28 | */ |
29 | |
30 | namespace VuFind\Auth; |
31 | |
32 | use Laminas\Http\PhpEnvironment\Request; |
33 | use VuFind\Db\Entity\UserEntityInterface; |
34 | use VuFind\Exception\Auth as AuthException; |
35 | |
36 | use function call_user_func_array; |
37 | use function func_get_args; |
38 | use function in_array; |
39 | use function is_callable; |
40 | use function strlen; |
41 | |
42 | /** |
43 | * ChoiceAuth Authentication plugin |
44 | * |
45 | * This module enables a user to choose between two authentication methods. |
46 | * choices are presented side-by-side and one is manually selected. |
47 | * |
48 | * See config.ini for more details |
49 | * |
50 | * @category VuFind |
51 | * @package Authentication |
52 | * @author Anna Headley <vufind-tech@lists.sourceforge.net> |
53 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
54 | * @link https://vufind.org/wiki/development:plugins:authentication_handlers Wiki |
55 | */ |
56 | class ChoiceAuth extends AbstractBase |
57 | { |
58 | /** |
59 | * Authentication strategies to present |
60 | * |
61 | * @var array |
62 | */ |
63 | protected $strategies = []; |
64 | |
65 | /** |
66 | * Auth strategy selected by user |
67 | * |
68 | * @var string |
69 | */ |
70 | protected $strategy; |
71 | |
72 | /** |
73 | * Plugin manager for obtaining other authentication objects |
74 | * |
75 | * @var PluginManager |
76 | */ |
77 | protected $manager; |
78 | |
79 | /** |
80 | * Session container |
81 | * |
82 | * @var \Laminas\Session\Container |
83 | */ |
84 | protected $session; |
85 | |
86 | /** |
87 | * Constructor |
88 | * |
89 | * @param \Laminas\Session\Container $container Session container for retaining |
90 | * user choices. |
91 | */ |
92 | public function __construct(\Laminas\Session\Container $container) |
93 | { |
94 | // Set up session container and load cached strategy (if found): |
95 | $this->session = $container; |
96 | $this->strategy = $this->session->auth_method ?? false; |
97 | } |
98 | |
99 | /** |
100 | * Validate configuration parameters. This is a support method for getConfig(), |
101 | * so the configuration MUST be accessed using $this->config; do not call |
102 | * $this->getConfig() from within this method! |
103 | * |
104 | * @throws AuthException |
105 | * @return void |
106 | */ |
107 | protected function validateConfig() |
108 | { |
109 | if ( |
110 | !isset($this->config->ChoiceAuth->choice_order) |
111 | || !strlen($this->config->ChoiceAuth->choice_order) |
112 | ) { |
113 | throw new AuthException( |
114 | 'One or more ChoiceAuth parameters are missing. ' . |
115 | 'Check your config.ini!' |
116 | ); |
117 | } |
118 | } |
119 | |
120 | /** |
121 | * Set configuration; throw an exception if it is invalid. |
122 | * |
123 | * @param \Laminas\Config\Config $config Configuration to set |
124 | * |
125 | * @throws AuthException |
126 | * @return void |
127 | */ |
128 | public function setConfig($config) |
129 | { |
130 | parent::setConfig($config); |
131 | $this->strategies = array_map( |
132 | 'trim', |
133 | explode(',', $this->getConfig()->ChoiceAuth->choice_order) |
134 | ); |
135 | } |
136 | |
137 | /** |
138 | * Inspect the user's request prior to processing a login request; this is |
139 | * essentially an event hook which most auth modules can ignore. See |
140 | * ChoiceAuth for a use case example. |
141 | * |
142 | * @param Request $request Request object. |
143 | * |
144 | * @throws AuthException |
145 | * @return void |
146 | */ |
147 | public function preLoginCheck($request) |
148 | { |
149 | $this->setStrategyFromRequest($request); |
150 | } |
151 | |
152 | /** |
153 | * Reset any internal status; this is essentially an event hook which most auth |
154 | * modules can ignore. See ChoiceAuth for a use case example. |
155 | * |
156 | * @return void |
157 | */ |
158 | public function resetState() |
159 | { |
160 | $this->strategy = false; |
161 | } |
162 | |
163 | /** |
164 | * Attempt to authenticate the current user. Throws exception if login fails. |
165 | * |
166 | * @param Request $request Request object containing account credentials. |
167 | * |
168 | * @throws AuthException |
169 | * @return UserEntityInterface Object representing logged-in user. |
170 | */ |
171 | public function authenticate($request) |
172 | { |
173 | try { |
174 | return $this->proxyUserLoad($request, 'authenticate', func_get_args()); |
175 | } catch (\Exception $e) { |
176 | // If an exception was thrown during login, we need to clear the |
177 | // stored strategy to ensure that we display the full ChoiceAuth |
178 | // form rather than the form for only the method that the user |
179 | // attempted to use. |
180 | $this->strategy = false; |
181 | throw $e; |
182 | } |
183 | } |
184 | |
185 | /** |
186 | * Create a new user account from the request. |
187 | * |
188 | * @param Request $request Request object containing new account details. |
189 | * |
190 | * @throws AuthException |
191 | * @return UserEntityInterface New user entity. |
192 | */ |
193 | public function create($request) |
194 | { |
195 | return $this->proxyUserLoad($request, 'create', func_get_args()); |
196 | } |
197 | |
198 | /** |
199 | * Set the manager for loading other authentication plugins. |
200 | * |
201 | * @param PluginManager $manager Plugin manager |
202 | * |
203 | * @return void |
204 | */ |
205 | public function setPluginManager(PluginManager $manager) |
206 | { |
207 | $this->manager = $manager; |
208 | } |
209 | |
210 | /** |
211 | * Get the manager for loading other authentication plugins. |
212 | * |
213 | * @throws \Exception |
214 | * @return PluginManager |
215 | */ |
216 | public function getPluginManager() |
217 | { |
218 | if (null === $this->manager) { |
219 | throw new \Exception('Plugin manager missing.'); |
220 | } |
221 | return $this->manager; |
222 | } |
223 | |
224 | /** |
225 | * Return an array of authentication options allowed by this class. |
226 | * |
227 | * @return array |
228 | */ |
229 | public function getSelectableAuthOptions() |
230 | { |
231 | return $this->strategies; |
232 | } |
233 | |
234 | /** |
235 | * If an authentication strategy has been selected, return the active option. |
236 | * If not, return false. |
237 | * |
238 | * @return bool|string |
239 | */ |
240 | public function getSelectedAuthOption() |
241 | { |
242 | return $this->strategy; |
243 | } |
244 | |
245 | /** |
246 | * Perform cleanup at logout time. |
247 | * |
248 | * @param string $url URL to redirect user to after logging out. |
249 | * |
250 | * @throws InvalidArgumentException |
251 | * @return string Redirect URL (usually same as $url, but modified in |
252 | * some authentication modules). |
253 | */ |
254 | public function logout($url) |
255 | { |
256 | // clear user's login choice, if necessary: |
257 | if (isset($this->session->auth_method)) { |
258 | unset($this->session->auth_method); |
259 | } |
260 | |
261 | // If we have a selected strategy, proxy the appropriate class; otherwise, |
262 | // perform default behavior of returning unmodified URL: |
263 | try { |
264 | return $this->strategy |
265 | ? $this->proxyAuthMethod('logout', func_get_args()) : $url; |
266 | } catch (InvalidArgumentException $e) { |
267 | // If we're in an invalid state (due to an illegal login method), |
268 | // we should just clear everything out so the user can try again. |
269 | $this->strategy = false; |
270 | return false; |
271 | } |
272 | } |
273 | |
274 | /** |
275 | * Get the URL to establish a session (needed when the internal VuFind login |
276 | * form is inadequate). Returns false when no session initiator is needed. |
277 | * |
278 | * @param string $target Full URL where external authentication strategy should |
279 | * send user after login (some drivers may override this). |
280 | * |
281 | * @return bool|string |
282 | */ |
283 | public function getSessionInitiator($target) |
284 | { |
285 | return $this->proxyAuthMethod('getSessionInitiator', func_get_args()); |
286 | } |
287 | |
288 | /** |
289 | * Does this authentication method support password changing |
290 | * |
291 | * @return bool |
292 | */ |
293 | public function supportsPasswordChange() |
294 | { |
295 | return $this->proxyAuthMethod('supportsPasswordChange', func_get_args()); |
296 | } |
297 | |
298 | /** |
299 | * Does this authentication method support password recovery |
300 | * |
301 | * @return bool |
302 | */ |
303 | public function supportsPasswordRecovery() |
304 | { |
305 | return $this->proxyAuthMethod('supportsPasswordRecovery', func_get_args()); |
306 | } |
307 | |
308 | /** |
309 | * Username policy for a new account (e.g. minLength, maxLength) |
310 | * |
311 | * @return array |
312 | */ |
313 | public function getUsernamePolicy() |
314 | { |
315 | return $this->proxyAuthMethod('getUsernamePolicy', func_get_args()); |
316 | } |
317 | |
318 | /** |
319 | * Password policy for a new password (e.g. minLength, maxLength) |
320 | * |
321 | * @return array |
322 | */ |
323 | public function getPasswordPolicy() |
324 | { |
325 | return $this->proxyAuthMethod('getPasswordPolicy', func_get_args()); |
326 | } |
327 | |
328 | /** |
329 | * Update a user's password from the request. |
330 | * |
331 | * @param Request $request Request object containing password change details. |
332 | * |
333 | * @throws AuthException |
334 | * @return UserEntityInterface Updated user entity. |
335 | */ |
336 | public function updatePassword($request) |
337 | { |
338 | // When a user is recovering a forgotten password, there may be an |
339 | // auth method included in the request since we haven't set an active |
340 | // strategy yet -- thus we should check for it. |
341 | $this->setStrategyFromRequest($request); |
342 | return $this->proxyAuthMethod('updatePassword', func_get_args()); |
343 | } |
344 | |
345 | /** |
346 | * Returns any authentication method this request should be delegated to. |
347 | * |
348 | * @param Request $request Request object. |
349 | * |
350 | * @return string|bool |
351 | * |
352 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
353 | */ |
354 | public function getDelegateAuthMethod(Request $request) |
355 | { |
356 | return $this->proxyAuthMethod('getDelegateAuthMethod', func_get_args()); |
357 | } |
358 | |
359 | /** |
360 | * Is the configured strategy on the list of legal options? |
361 | * |
362 | * @return bool |
363 | */ |
364 | protected function hasLegalStrategy() |
365 | { |
366 | // Do a case-insensitive search of the strategy list: |
367 | return in_array( |
368 | strtolower($this->strategy), |
369 | array_map('strtolower', $this->strategies) |
370 | ); |
371 | } |
372 | |
373 | /** |
374 | * Proxy auth method; a helper function to be called like: |
375 | * return $this->proxyAuthMethod(METHOD, func_get_args()); |
376 | * |
377 | * @param string $method the method to proxy |
378 | * @param array $params array of params to pass |
379 | * |
380 | * @throws AuthException |
381 | * @return mixed |
382 | */ |
383 | protected function proxyAuthMethod($method, $params) |
384 | { |
385 | // If no strategy is found, we can't do anything -- return false. |
386 | if (!$this->strategy) { |
387 | return false; |
388 | } |
389 | |
390 | if (!$this->hasLegalStrategy()) { |
391 | throw new InvalidArgumentException("Illegal setting: {$this->strategy}"); |
392 | } |
393 | $authenticator = $this->getPluginManager()->get($this->strategy); |
394 | $authenticator->setConfig($this->getConfig()); |
395 | if (!is_callable([$authenticator, $method])) { |
396 | throw new AuthException($this->strategy . "has no method $method"); |
397 | } |
398 | return call_user_func_array([$authenticator, $method], $params); |
399 | } |
400 | |
401 | /** |
402 | * Proxy auth method that checks the request for an active method and then |
403 | * loads a UserEntityInterface object from the database (e.g. authenticate or create). |
404 | * |
405 | * @param Request $request Request object to check. |
406 | * @param string $method the method to proxy |
407 | * @param array $params array of params to pass |
408 | * |
409 | * @throws AuthException |
410 | * @return mixed |
411 | */ |
412 | protected function proxyUserLoad($request, $method, $params) |
413 | { |
414 | $this->setStrategyFromRequest($request); |
415 | $user = $this->proxyAuthMethod($method, $params); |
416 | if (!$user) { |
417 | throw new AuthException('Unexpected return value'); |
418 | } |
419 | $this->session->auth_method = $this->strategy; |
420 | return $user; |
421 | } |
422 | |
423 | /** |
424 | * Set the active strategy based on the auth_method value in the request, |
425 | * if found. |
426 | * |
427 | * @param Request $request Request object to check. |
428 | * |
429 | * @return void |
430 | */ |
431 | protected function setStrategyFromRequest($request) |
432 | { |
433 | // Set new strategy; fall back to old one if there is a problem: |
434 | $defaultStrategy = $this->strategy; |
435 | $this->strategy = trim($request->getPost()->get('auth_method', '')); |
436 | if (!$this->strategy) { |
437 | $this->strategy = trim($request->getQuery()->get('auth_method', '')); |
438 | } |
439 | if (!$this->strategy || !in_array($this->strategy, $this->strategies)) { |
440 | $this->strategy = $defaultStrategy; |
441 | if (empty($this->strategy)) { |
442 | throw new AuthException('authentication_error_technical'); |
443 | } |
444 | } |
445 | } |
446 | |
447 | /** |
448 | * Set the active strategy |
449 | * |
450 | * @param string $strategy New strategy |
451 | * |
452 | * @return void |
453 | */ |
454 | public function setStrategy($strategy) |
455 | { |
456 | $this->strategy = $strategy; |
457 | $this->session->auth_method = $strategy; |
458 | } |
459 | |
460 | /** |
461 | * Validate the credentials in the provided request, but do not change the state |
462 | * of the current logged-in user. Return true for valid credentials, false |
463 | * otherwise. |
464 | * |
465 | * @param Request $request Request object containing account credentials. |
466 | * |
467 | * @throws AuthException |
468 | * @return bool |
469 | */ |
470 | public function validateCredentials($request) |
471 | { |
472 | try { |
473 | // In this instance we are checking credentials but do not wish to |
474 | // change the state of the current object. Thus, we use proxyAuthMethod() |
475 | // here instead of proxyUserLoad(). |
476 | $user = $this->proxyAuthMethod('authenticate', func_get_args()); |
477 | } catch (AuthException $e) { |
478 | return false; |
479 | } |
480 | return isset($user) && $user instanceof UserEntityInterface; |
481 | } |
482 | |
483 | /** |
484 | * Whether this authentication method needs CSRF checking for the request. |
485 | * |
486 | * @param Request $request Request object. |
487 | * |
488 | * @return bool |
489 | * |
490 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
491 | */ |
492 | public function needsCsrfCheck($request) |
493 | { |
494 | if (!$this->strategy) { |
495 | return true; |
496 | } |
497 | return $this->proxyAuthMethod('needsCsrfCheck', func_get_args()); |
498 | } |
499 | } |