Issues (1474)

framework/Security/TAuthManager.php (12 issues)

1
<?php
2
3
/**
4
 * TAuthManager class file
5
 *
6
 * @author Qiang Xue <[email protected]>
7
 * @link https://github.com/pradosoft/prado
8
 * @license https://github.com/pradosoft/prado/blob/master/LICENSE
9
 */
10
11
namespace Prado\Security;
12
13
use Prado\Exceptions\TConfigurationException;
14
use Prado\Exceptions\TInvalidOperationException;
15
use Prado\TPropertyValue;
16
use Prado\Web\Services\TPageService;
17
use Prado\Web\THttpCookie;
18
19
/**
20
 * TAuthManager class
21
 *
22
 * TAuthManager performs user authentication and authorization for a Prado application.
23
 * TAuthManager works together with a {@see \Prado\Security\IUserManager} module that can be
24
 * specified via the {@see setUserManager UserManager} property.
25
 * If an authorization fails, TAuthManager will try to redirect the client
26
 * browser to a login page that is specified via the {@see setLoginPage LoginPage}.
27
 * To login or logout a user, call {@see login} or {@see logout}, respectively.
28
 *
29
 * The {@see setAuthExpire AuthExpire} property can be used to define the time
30
 * in seconds after which the authentication should expire.
31
 * {@see setAllowAutoLogin AllowAutoLogin} specifies if the login information
32
 * should be stored in a cookie to perform automatic login. Enabling this
33
 * feature will cause that {@see setAuthExpire AuthExpire} has no effect
34
 * since the user will be logged in again on authentication expiration.
35
 *
36
 * To load TAuthManager, configure it in application configuration as follows,
37
 * <module id="auth" class="Prado\Security\TAuthManager" UserManager="users" LoginPage="login" />
38
 * <module id="users" class="Prado\Security\TUserManager" />
39
 *
40
 * When a user logs in, onLogin event is raised with the TUser as the parameter.
41
 * If the user trying to login but fails the check, onLoginFailed is raised with the
42
 * user name as parameter.  When the user logs out, onLogout is raised with the TUser
43
 * as parameter.
44
 *
45
 * @author Qiang Xue <[email protected]>
46
 * @since 3.0
47
 */
48
class TAuthManager extends \Prado\TModule
49
{
50
	/**
51
	 * GET variable name for return url
52
	 */
53
	public const RETURN_URL_VAR = 'ReturnUrl';
54
	/**
55
	 * @var bool if the module has been initialized
56
	 */
57
	private $_initialized = false;
58
	/**
59
	 * @var IUserManager user manager instance
60
	 */
61
	private $_userManager;
62
	/**
63
	 * @var string login page
64
	 */
65
	private $_loginPage;
66
	/**
67
	 * @var bool whether authorization should be skipped
68
	 */
69
	private $_skipAuthorization = false;
70
	/**
71
	 * @var string the session var name for storing return URL
72
	 */
73
	private $_returnUrlVarName;
74
	/**
75
	 * @var bool whether to allow auto login (using cookie)
76
	 */
77
	private $_allowAutoLogin = false;
78
	/**
79
	 * @var string variable name used to store user session or cookie
80
	 */
81
	private $_userKey;
82
	/**
83
	 * @var int authentication expiration time in seconds. Defaults to zero (no expiration)
84
	 */
85
	private $_authExpire = 0;
86
87
	/**
88
	 * Initializes this module.
89 3
	 * This method is required by the IModule interface.
90
	 * @param \Prado\Xml\TXmlElement $config configuration for this module, can be null
91 3
	 * @throws TConfigurationException if user manager does not exist or is not IUserManager
92 1
	 */
93
	public function init($config)
94 3
	{
95 3
		if ($this->_userManager === null) {
96
			throw new TConfigurationException('authmanager_usermanager_required');
97 3
		}
98 3
		if ($this->_returnUrlVarName === null) {
99 3
			$this->_returnUrlVarName = $this->getApplication()->getID() . ':' . self::RETURN_URL_VAR;
100
		}
101
		$application = $this->getApplication();
102 3
		if (is_string($this->_userManager)) {
0 ignored issues
show
The condition is_string($this->_userManager) is always false.
Loading history...
103
			if (($users = $application->getModule($this->_userManager)) === null) {
104
				throw new TConfigurationException('authmanager_usermanager_inexistent', $this->_userManager);
105 3
			}
106
			if (!($users instanceof IUserManager)) {
107 3
				throw new TConfigurationException('authmanager_usermanager_invalid', $this->_userManager);
108 3
			}
109 3
			$this->_userManager = $users;
110 3
		}
111 3
		$application->attachEventHandler('OnAuthentication', [$this, 'doAuthentication']);
112
		$application->attachEventHandler('OnEndRequest', [$this, 'leave']);
113
		$application->attachEventHandler('OnAuthorization', [$this, 'doAuthorization']);
114
		$this->_initialized = true;
115
		parent::init($config);
116 2
	}
117
118 2
	/**
119
	 * @return IUserManager user manager instance
120
	 */
121
	public function getUserManager()
122
	{
123
		return $this->_userManager;
124
	}
125 3
126
	/**
127 3
	 * @param IUserManager|string $provider the user manager module ID or the user manager object
128 1
	 * @throws TInvalidOperationException if the module has been initialized or the user manager object is not IUserManager
129
	 */
130 3
	public function setUserManager($provider)
131
	{
132
		if ($this->_initialized) {
133 3
			throw new TInvalidOperationException('authmanager_usermanager_unchangeable');
134 3
		}
135
		if (!is_string($provider) && !($provider instanceof IUserManager)) {
0 ignored issues
show
$provider is always a sub-type of Prado\Security\IUserManager.
Loading history...
136
			throw new TConfigurationException('authmanager_usermanager_invalid', $provider);
137
		}
138
		$this->_userManager = $provider;
0 ignored issues
show
Documentation Bug introduced by
It seems like $provider can also be of type string. However, the property $_userManager is declared as type Prado\Security\IUserManager. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
139 1
	}
140
141 1
	/**
142
	 * @return string path of login page should login is required
143
	 */
144
	public function getLoginPage()
145
	{
146
		return $this->_loginPage;
147
	}
148
149
	/**
150 1
	 * Sets the login page that the client browser will be redirected to if login is needed.
151
	 * Login page should be specified in the format of page path.
152 1
	 * @param string $pagePath path of login page should login is required
153 1
	 * @see TPageService
154
	 */
155
	public function setLoginPage($pagePath)
156
	{
157
		$this->_loginPage = $pagePath;
158
	}
159
160
	/**
161
	 * Performs authentication.
162
	 * This is the event handler attached to application's Authentication event.
163
	 * Do not call this method directly.
164
	 * @param mixed $sender sender of the Authentication event
165
	 * @param mixed $param event parameter
166
	 */
167
	public function doAuthentication($sender, $param)
168
	{
169
		$this->onAuthenticate($param);
170
171
		$service = $this->getService();
172
		if (($service instanceof TPageService) && $service->getRequestedPagePath() === $this->getLoginPage()) {
173
			$this->_skipAuthorization = true;
174
		}
175
	}
176
177
	/**
178
	 * Performs authorization.
179
	 * This is the event handler attached to application's Authorization event.
180
	 * Do not call this method directly.
181
	 * @param mixed $sender sender of the Authorization event
182
	 * @param mixed $param event parameter
183
	 */
184
	public function doAuthorization($sender, $param)
185
	{
186
		if (!$this->_skipAuthorization) {
187
			$this->onAuthorize($param);
188
		}
189
	}
190
191
	/**
192
	 * Performs login redirect if authorization fails.
193
	 * This is the event handler attached to application's EndRequest event.
194
	 * Do not call this method directly.
195
	 * @param mixed $sender sender of the event
196
	 * @param mixed $param event parameter
197
	 */
198
	public function leave($sender, $param)
0 ignored issues
show
The parameter $sender is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

198
	public function leave(/** @scrutinizer ignore-unused */ $sender, $param)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
The parameter $param is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

198
	public function leave($sender, /** @scrutinizer ignore-unused */ $param)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
199
	{
200
		$application = $this->getApplication();
201
		if ($application->getResponse()->getStatusCode() === 401) {
202
			$service = $application->getService();
203
			if ($service instanceof TPageService) {
204
				$returnUrl = $application->getRequest()->getRequestUri();
205
				$this->setReturnUrl($returnUrl);
206
				$url = $service->constructUrl($this->getLoginPage());
207
				$application->getResponse()->redirect($url);
208
			}
209
		}
210
	}
211
212
	/**
213
	 * @return string the name of the session variable storing return URL. It defaults to 'AppID:ReturnUrl'
214
	 */
215
	public function getReturnUrlVarName()
216
	{
217
		return $this->_returnUrlVarName;
218
	}
219
220
	/**
221
	 * @param string $value the name of the session variable storing return URL.
222
	 */
223
	public function setReturnUrlVarName($value)
224
	{
225
		$this->_returnUrlVarName = $value;
226
	}
227
228
	/**
229
	 * @return string URL that the browser should be redirected to when login succeeds.
230
	 */
231
	public function getReturnUrl()
232
	{
233
		return $this->getSession()->itemAt($this->getReturnUrlVarName());
234
	}
235
236
	/**
237
	 * Sets the URL that the browser should be redirected to when login succeeds.
238
	 * @param string $value the URL to be redirected to.
239
	 */
240
	public function setReturnUrl($value)
241
	{
242
		$this->getSession()->add($this->getReturnUrlVarName(), $value);
243
	}
244
245
	/**
246
	 * @return bool whether to allow remembering login so that the user logs on automatically next time. Defaults to false.
247
	 * @since 3.1.1
248
	 */
249
	public function getAllowAutoLogin()
250
	{
251
		return $this->_allowAutoLogin;
252
	}
253
254
	/**
255
	 * @param bool $value whether to allow remembering login so that the user logs on automatically next time. Users have to enable cookie to make use of this feature.
256
	 * @since 3.1.1
257
	 */
258
	public function setAllowAutoLogin($value)
259
	{
260
		$this->_allowAutoLogin = TPropertyValue::ensureBoolean($value);
261
	}
262
263
	/**
264
	 * @return int authentication expiration time in seconds. Defaults to zero (no expiration).
265
	 * @since 3.1.3
266
	 */
267
	public function getAuthExpire()
268
	{
269
		return $this->_authExpire;
270
	}
271
272
	/**
273
	 * @param int $value authentication expiration time in seconds. Defaults to zero (no expiration).
274
	 * @since 3.1.3
275
	 */
276
	public function setAuthExpire($value)
277
	{
278
		$this->_authExpire = TPropertyValue::ensureInteger($value);
279
	}
280
281
	/**
282
	 * Performs the real authentication work.
283
	 * An OnAuthenticate event will be raised if there is any handler attached to it.
284
	 * If the application already has a non-null user, it will return without further authentication.
285
	 * Otherwise, user information will be restored from session data.
286
	 * @param mixed $param parameter to be passed to OnAuthenticate event
287
	 * @throws TConfigurationException if session module does not exist.
288
	 */
289
	public function onAuthenticate($param)
290
	{
291
		$application = $this->getApplication();
292
293
		// restoring user info from session
294
		if (($session = $application->getSession()) === null) {
295
			throw new TConfigurationException('authmanager_session_required');
296
		}
297
		$session->open();
298
		$sessionInfo = $session->itemAt($this->getUserKey());
299
		$user = $this->_userManager->getUser(null)->loadFromString($sessionInfo);
300
301
		// check for authentication expiration
302
		$isAuthExpired = $this->_authExpire > 0 && !$user->getIsGuest() &&
303
		($expiretime = $session->itemAt('AuthExpireTime')) && $expiretime < time();
304
305
		// try authenticating through cookie if possible
306
		if ($this->getAllowAutoLogin() && ($user->getIsGuest() || $isAuthExpired)) {
307
			$cookie = $this->getRequest()->getCookies()->itemAt($this->getUserKey());
308
			if ($cookie instanceof THttpCookie) {
0 ignored issues
show
$cookie is always a sub-type of Prado\Web\THttpCookie.
Loading history...
309
				if (($user2 = $this->_userManager->getUserFromCookie($cookie)) !== null) {
310
					$user = $user2;
311
					$this->updateSessionUser($user);
312
					// user is restored from cookie, auth may not expire
313
					$isAuthExpired = false;
314
				}
315
			}
316
		}
317
318
		$application->setUser($user);
319
320
		// handle authentication expiration or update expiration time
321
		if ($isAuthExpired) {
322
			$this->onAuthExpire($param);
323
		} else {
324
			$session->add('AuthExpireTime', time() + $this->_authExpire);
325
		}
326
327
		// event handler gets a chance to do further auth work
328
		if ($this->hasEventHandler('OnAuthenticate')) {
329
			$this->raiseEvent('OnAuthenticate', $this, $application);
0 ignored issues
show
$application of type Prado\TApplication is incompatible with the type Prado\TEventParameter expected by parameter $param of Prado\TComponent::raiseEvent(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

329
			$this->raiseEvent('OnAuthenticate', $this, /** @scrutinizer ignore-type */ $application);
Loading history...
330
		}
331
	}
332
333
	/**
334
	 * Performs user logout on authentication expiration.
335
	 * An 'OnAuthExpire' event will be raised if there is any handler attached to it.
336
	 * @param mixed $param parameter to be passed to OnAuthExpire event.
337
	 */
338
	public function onAuthExpire($param)
339
	{
340
		$this->logout();
341
		if ($this->hasEventHandler('OnAuthExpire')) {
342
			$this->raiseEvent('OnAuthExpire', $this, $param);
343
		}
344
	}
345
346
	/**
347
	 * Performs the real authorization work.
348
	 * Authorization rules obtained from the application will be used to check
349
	 * if a user is allowed. If authorization fails, the response status code
350
	 * will be set as 401 and the application terminates.
351
	 * @param mixed $param parameter to be passed to OnAuthorize event
352
	 */
353
	public function onAuthorize($param)
0 ignored issues
show
The parameter $param is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

353
	public function onAuthorize(/** @scrutinizer ignore-unused */ $param)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
354
	{
355
		$application = $this->getApplication();
356
		if ($this->hasEventHandler('OnAuthorize')) {
357
			$this->raiseEvent('OnAuthorize', $this, $application);
0 ignored issues
show
$application of type Prado\TApplication is incompatible with the type Prado\TEventParameter expected by parameter $param of Prado\TComponent::raiseEvent(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

357
			$this->raiseEvent('OnAuthorize', $this, /** @scrutinizer ignore-type */ $application);
Loading history...
358
		}
359
		if (!$application->getAuthorizationRules()->isUserAllowed($application->getUser(), $application->getRequest()->getRequestType(), $application->getRequest()->getUserHostAddress())) {
360
			$application->getResponse()->setStatusCode(401);
361
			$application->completeRequest();
362
		}
363
	}
364
365
	/**
366
	 * @return string a unique variable name for storing user session/cookie data
367
	 * @since 3.1.1
368
	 */
369
	public function getUserKey()
370
	{
371
		if ($this->_userKey === null) {
372
			$this->_userKey = $this->generateUserKey();
373
		}
374
		return $this->_userKey;
375
	}
376
377
	/**
378
	 * @return string a key used to store user information in session
379
	 * @since 3.1.1
380
	 */
381
	protected function generateUserKey()
382
	{
383
		return md5($this->getApplication()->getUniqueID() . 'prado:user');
384
	}
385
386
	/**
387
	 * Updates the user data stored in session.
388
	 * @param IUser $user user object
389
	 * @throws TConfigurationException if session module is not loaded.
390
	 */
391
	public function updateSessionUser($user)
392
	{
393
		if (php_sapi_name() !== 'cli' && !$user->getIsGuest()) {
394
			if (($session = $this->getSession()) === null) {
395
				throw new TConfigurationException('authmanager_session_required');
396
			} else {
397
				$session->add($this->getUserKey(), $user->saveToString());
398
				$session->regenerate(true);
399
			}
400
		}
401
	}
402
403
	/**
404
	 * Switches to a new user.
405
	 * This method will logout the current user first and login with a new one (without password.)
406
	 * @param string $username the new username
407
	 * @return bool if the switch is successful
408
	 */
409
	public function switchUser($username)
410
	{
411
		if (($user = $this->_userManager->getUser($username)) === null) {
412
			return false;
413
		}
414
		$this->updateSessionUser($user);
415
		$this->getApplication()->setUser($user);
416
		return true;
417
	}
418
419
	/**
420
	 * Logs in a user with username and password.
421
	 * The username and password will be used to validate if login is successful.
422
	 * If yes, a user object will be created for the application.
423
	 * On successful Login, onLogin is raised with the TUser as parameter.
424
	 * When the login fails, onLoginFailed is raised with the username as parameter.
425
	 * @param string $username username
426
	 * @param string $password password
427
	 * @param int $expire number of seconds that automatic login will remain effective. If 0, it means user logs out when session ends. This parameter is added since 3.1.1.
428
	 * @return bool if login is successful
429
	 */
430
	public function login($username, #[\SensitiveParameter] $password, $expire = 0)
431
	{
432
		if ($this->_userManager->validateUser($username, $password)) {
433
			if (($user = $this->_userManager->getUser($username)) === null) {
434
				return false;
435
			}
436
			$this->updateSessionUser($user);
437
			$this->getApplication()->setUser($user);
438
439
			if ($expire > 0) {
440
				$cookie = new THttpCookie($this->getUserKey(), '');
441
				$cookie->setExpire(time() + $expire);
442
				$this->_userManager->saveUserToCookie($cookie);
443
				$this->getResponse()->getCookies()->add($cookie);
444
			}
445
			$this->onLogin($user);
446
			return true;
447
		} else {
448
			$this->onLoginFailed($username);
449
			return false;
450
		}
451
	}
452
453
	/**
454
	 * Logs out a user.  Raises onLogout with the TUser as parameter
455
	 * before logging out. User session will be destroyed after this
456
	 * method is called.
457
	 * @throws TConfigurationException if session module is not loaded.
458
	 */
459
	public function logout()
460
	{
461
		$this->onLogout($this->getApplication()->getUser());
462
		if (($session = $this->getSession()) === null) {
463
			throw new TConfigurationException('authmanager_session_required');
464
		}
465
		$this->getApplication()->getUser()->setIsGuest(true);
466
		$session->destroy();
467
		if ($this->getAllowAutoLogin()) {
468
			$cookie = new THttpCookie($this->getUserKey(), '');
469
			$this->getResponse()->getCookies()->add($cookie);
470
		}
471
	}
472
473
	/**
474
	 * onLogin event is raised when a user logs in
475
	 * @param TUser $user user being logged in
476
	 * @since 4.2.0
477
	 */
478
	public function onLogin($user)
479
	{
480
		$this->raiseEvent('onLogin', $this, $user);
0 ignored issues
show
$user of type Prado\Security\TUser is incompatible with the type Prado\TEventParameter expected by parameter $param of Prado\TComponent::raiseEvent(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

480
		$this->raiseEvent('onLogin', $this, /** @scrutinizer ignore-type */ $user);
Loading history...
481
	}
482
483
	/**
484
	 * onLoginFailed event is raised when a user login fails
485
	 * @param string $username username trying to log in
486
	 * @since 4.2.0
487
	 */
488
	public function onLoginFailed($username)
489
	{
490
		$this->raiseEvent('onLoginFailed', $this, $username);
0 ignored issues
show
$username of type string is incompatible with the type Prado\TEventParameter expected by parameter $param of Prado\TComponent::raiseEvent(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

490
		$this->raiseEvent('onLoginFailed', $this, /** @scrutinizer ignore-type */ $username);
Loading history...
491
	}
492
493
	/**
494
	 * onLogout event is raised when a user logs out.
495
	 * @param TUser $user user being logged out
496
	 * @since 4.2.0
497
	 */
498
	public function onLogout($user)
499
	{
500
		$this->raiseEvent('onLogout', $this, $user);
0 ignored issues
show
$user of type Prado\Security\TUser is incompatible with the type Prado\TEventParameter expected by parameter $param of Prado\TComponent::raiseEvent(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

500
		$this->raiseEvent('onLogout', $this, /** @scrutinizer ignore-type */ $user);
Loading history...
501
	}
502
}
503