Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
72.73% covered (warning)
72.73%
56 / 77
53.85% covered (warning)
53.85%
14 / 26
CRAP
0.00% covered (danger)
0.00%
0 / 1
ChoiceAuth
72.73% covered (warning)
72.73%
56 / 77
53.85% covered (warning)
53.85%
14 / 26
83.27
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 validateConfig
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 setConfig
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 preLoginCheck
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 resetState
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 authenticate
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 create
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setPluginManager
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPluginManager
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getSelectableAuthOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSelectedAuthOption
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 logout
57.14% covered (warning)
57.14%
4 / 7
0.00% covered (danger)
0.00%
0 / 1
5.26
 getSessionInitiator
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 supportsPasswordChange
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 supportsPasswordRecovery
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUsernamePolicy
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPasswordPolicy
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 updatePassword
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getDelegateAuthMethod
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 hasLegalStrategy
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 proxyAuthMethod
77.78% covered (warning)
77.78%
7 / 9
0.00% covered (danger)
0.00%
0 / 1
4.18
 proxyUserLoad
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
2.02
 setStrategyFromRequest
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 setStrategy
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 validateCredentials
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 needsCsrfCheck
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
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
30namespace VuFind\Auth;
31
32use Laminas\Http\PhpEnvironment\Request;
33use VuFind\Db\Entity\UserEntityInterface;
34use VuFind\Exception\Auth as AuthException;
35
36use function call_user_func_array;
37use function func_get_args;
38use function in_array;
39use function is_callable;
40use 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 */
56class 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}