summaryrefslogtreecommitdiff
path: root/lib/prado/framework/Security/TAuthManager.php
blob: 52d30d901f6ea88be0f9ae65e4bda03e93fc6873 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
<?php
/**
 * TAuthManager class file
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @link https://github.com/pradosoft/prado
 * @copyright Copyright &copy; 2005-2015 The PRADO Group
 * @license https://github.com/pradosoft/prado/blob/master/COPYRIGHT
 * @package System.Security
 */

/**
 * Using IUserManager interface
 */
Prado::using('System.Security.IUserManager');

/**
 * TAuthManager class
 *
 * TAuthManager performs user authentication and authorization for a Prado application.
 * TAuthManager works together with a {@link IUserManager} module that can be
 * specified via the {@link setUserManager UserManager} property.
 * If an authorization fails, TAuthManager will try to redirect the client
 * browser to a login page that is specified via the {@link setLoginPage LoginPage}.
 * To login or logout a user, call {@link login} or {@link logout}, respectively.
 *
 * The {@link setAuthExpire AuthExpire} property can be used to define the time
 * in seconds after which the authentication should expire.
 * {@link setAllowAutoLogin AllowAutoLogin} specifies if the login information
 * should be stored in a cookie to perform automatic login. Enabling this
 * feature will cause that {@link setAuthExpire AuthExpire} has no effect
 * since the user will be logged in again on authentication expiration.
 *
 * To load TAuthManager, configure it in application configuration as follows,
 * <module id="auth" class="System.Security.TAuthManager" UserManager="users" LoginPage="login" />
 * <module id="users" class="System.Security.TUserManager" />
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @package System.Security
 * @since 3.0
 */
class TAuthManager extends TModule
{
	/**
	 * GET variable name for return url
	 */
	const RETURN_URL_VAR='ReturnUrl';
	/**
	 * @var boolean if the module has been initialized
	 */
	private $_initialized=false;
	/**
	 * @var IUserManager user manager instance
	 */
	private $_userManager;
	/**
	 * @var string login page
	 */
	private $_loginPage;
	/**
	 * @var boolean whether authorization should be skipped
	 */
	private $_skipAuthorization=false;
	/**
	 * @var string the session var name for storing return URL
	 */
	private $_returnUrlVarName;
	/**
	 * @var boolean whether to allow auto login (using cookie)
	 */
	private $_allowAutoLogin=false;
	/**
	 * @var string variable name used to store user session or cookie
	 */
	private $_userKey;
	/**
	 * @var integer authentication expiration time in seconds. Defaults to zero (no expiration)
	 */
	private $_authExpire=0;

	/**
	 * Initializes this module.
	 * This method is required by the IModule interface.
	 * @param TXmlElement configuration for this module, can be null
	 * @throws TConfigurationException if user manager does not exist or is not IUserManager
	 */
	public function init($config)
	{
		if($this->_userManager===null)
			throw new TConfigurationException('authmanager_usermanager_required');
		if($this->_returnUrlVarName===null)
			$this->_returnUrlVarName=$this->getApplication()->getID().':'.self::RETURN_URL_VAR;
		$application=$this->getApplication();
		if(is_string($this->_userManager))
		{
			if(($users=$application->getModule($this->_userManager))===null)
				throw new TConfigurationException('authmanager_usermanager_inexistent',$this->_userManager);
			if(!($users instanceof IUserManager))
				throw new TConfigurationException('authmanager_usermanager_invalid',$this->_userManager);
			$this->_userManager=$users;
		}
		$application->attachEventHandler('OnAuthentication',array($this,'doAuthentication'));
		$application->attachEventHandler('OnEndRequest',array($this,'leave'));
		$application->attachEventHandler('OnAuthorization',array($this,'doAuthorization'));
		$this->_initialized=true;
	}

	/**
	 * @return IUserManager user manager instance
	 */
	public function getUserManager()
	{
		return $this->_userManager;
	}

	/**
	 * @param string|IUserManager the user manager module ID or the user manager object
	 * @throws TInvalidOperationException if the module has been initialized or the user manager object is not IUserManager
	 */
	public function setUserManager($provider)
	{
		if($this->_initialized)
			throw new TInvalidOperationException('authmanager_usermanager_unchangeable');
		if(!is_string($provider) && !($provider instanceof IUserManager))
			throw new TConfigurationException('authmanager_usermanager_invalid',$this->_userManager);
		$this->_userManager=$provider;
	}

	/**
	 * @return string path of login page should login is required
	 */
	public function getLoginPage()
	{
		return $this->_loginPage;
	}

	/**
	 * Sets the login page that the client browser will be redirected to if login is needed.
	 * Login page should be specified in the format of page path.
	 * @param string path of login page should login is required
	 * @see TPageService
	 */
	public function setLoginPage($pagePath)
	{
		$this->_loginPage=$pagePath;
	}

	/**
	 * Performs authentication.
	 * This is the event handler attached to application's Authentication event.
	 * Do not call this method directly.
	 * @param mixed sender of the Authentication event
	 * @param mixed event parameter
	 */
	public function doAuthentication($sender,$param)
	{
		$this->onAuthenticate($param);

		$service=$this->getService();
		if(($service instanceof TPageService) && $service->getRequestedPagePath()===$this->getLoginPage())
			$this->_skipAuthorization=true;
	}

	/**
	 * Performs authorization.
	 * This is the event handler attached to application's Authorization event.
	 * Do not call this method directly.
	 * @param mixed sender of the Authorization event
	 * @param mixed event parameter
	 */
	public function doAuthorization($sender,$param)
	{
		if(!$this->_skipAuthorization)
		{
			$this->onAuthorize($param);
		}
	}

	/**
	 * Performs login redirect if authorization fails.
	 * This is the event handler attached to application's EndRequest event.
	 * Do not call this method directly.
	 * @param mixed sender of the event
	 * @param mixed event parameter
	 */
	public function leave($sender,$param)
	{
		$application=$this->getApplication();
		if($application->getResponse()->getStatusCode()===401)
		{
			$service=$application->getService();
			if($service instanceof TPageService)
			{
				$returnUrl=$application->getRequest()->getRequestUri();
				$this->setReturnUrl($returnUrl);
				$url=$service->constructUrl($this->getLoginPage());
				$application->getResponse()->redirect($url);
			}
		}
	}

	/**
	 * @return string the name of the session variable storing return URL. It defaults to 'AppID:ReturnUrl'
	 */
	public function getReturnUrlVarName()
	{
		return $this->_returnUrlVarName;
	}

	/**
	 * @param string the name of the session variable storing return URL.
	 */
	public function setReturnUrlVarName($value)
	{
		$this->_returnUrlVarName=$value;
	}

	/**
	 * @return string URL that the browser should be redirected to when login succeeds.
	 */
	public function getReturnUrl()
	{
		return $this->getSession()->itemAt($this->getReturnUrlVarName());
	}

	/**
	 * Sets the URL that the browser should be redirected to when login succeeds.
	 * @param string the URL to be redirected to.
	 */
	public function setReturnUrl($value)
	{
		$this->getSession()->add($this->getReturnUrlVarName(),$value);
	}

	/**
	 * @return boolean whether to allow remembering login so that the user logs on automatically next time. Defaults to false.
	 * @since 3.1.1
	 */
	public function getAllowAutoLogin()
	{
		return $this->_allowAutoLogin;
	}

	/**
	 * @param boolean 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.
	 * @since 3.1.1
	 */
	public function setAllowAutoLogin($value)
	{
		$this->_allowAutoLogin=TPropertyValue::ensureBoolean($value);
	}

	/**
	 * @return integer authentication expiration time in seconds. Defaults to zero (no expiration).
	 * @since 3.1.3
	 */
	public function getAuthExpire()
	{
		return $this->_authExpire;
	}

	/**
	 * @param integer authentication expiration time in seconds. Defaults to zero (no expiration).
	 * @since 3.1.3
	 */
	public function setAuthExpire($value)
	{
		$this->_authExpire=TPropertyValue::ensureInteger($value);
	}

	/**
	 * Performs the real authentication work.
	 * An OnAuthenticate event will be raised if there is any handler attached to it.
	 * If the application already has a non-null user, it will return without further authentication.
	 * Otherwise, user information will be restored from session data.
	 * @param mixed parameter to be passed to OnAuthenticate event
	 * @throws TConfigurationException if session module does not exist.
	 */
	public function onAuthenticate($param)
	{
		$application=$this->getApplication();

		// restoring user info from session
		if(($session=$application->getSession())===null)
			throw new TConfigurationException('authmanager_session_required');
		$session->open();
		$sessionInfo=$session->itemAt($this->getUserKey());
		$user=$this->_userManager->getUser(null)->loadFromString($sessionInfo);

		// check for authentication expiration
		$isAuthExpired = $this->_authExpire>0 && !$user->getIsGuest() &&
        ($expiretime=$session->itemAt('AuthExpireTime')) && $expiretime<time();

		// try authenticating through cookie if possible
		if($this->getAllowAutoLogin() && ($user->getIsGuest() || $isAuthExpired))
		{
			$cookie=$this->getRequest()->getCookies()->itemAt($this->getUserKey());
			if($cookie instanceof THttpCookie)
			{
				if(($user2=$this->_userManager->getUserFromCookie($cookie))!==null)
				{
					$user=$user2;
					$this->updateSessionUser($user);
					// user is restored from cookie, auth may not expire
					$isAuthExpired = false;
				}
			}
		}

		$application->setUser($user);

		// handle authentication expiration or update expiration time
		if($isAuthExpired)
			$this->onAuthExpire($param);
		else
			$session->add('AuthExpireTime', time() + $this->_authExpire);

		// event handler gets a chance to do further auth work
		if($this->hasEventHandler('OnAuthenticate'))
			$this->raiseEvent('OnAuthenticate',$this,$application);
	}

	/**
	 * Performs user logout on authentication expiration.
	 * An 'OnAuthExpire' event will be raised if there is any handler attached to it.
	 * @param mixed parameter to be passed to OnAuthExpire event.
	 */
	public function onAuthExpire($param)
	{
		$this->logout();
		if($this->hasEventHandler('OnAuthExpire'))
			$this->raiseEvent('OnAuthExpire',$this,$param);
	}

	/**
	 * Performs the real authorization work.
	 * Authorization rules obtained from the application will be used to check
	 * if a user is allowed. If authorization fails, the response status code
	 * will be set as 401 and the application terminates.
	 * @param mixed parameter to be passed to OnAuthorize event
	 */
	public function onAuthorize($param)
	{
		$application=$this->getApplication();
		if($this->hasEventHandler('OnAuthorize'))
			$this->raiseEvent('OnAuthorize',$this,$application);
		if(!$application->getAuthorizationRules()->isUserAllowed($application->getUser(),$application->getRequest()->getRequestType(),$application->getRequest()->getUserHostAddress()))
		{
			$application->getResponse()->setStatusCode(401);
			$application->completeRequest();
		}
	}

	/**
	 * @return string a unique variable name for storing user session/cookie data
	 * @since 3.1.1
	 */
	public function getUserKey()
	{
		if($this->_userKey===null)
			$this->_userKey=$this->generateUserKey();
		return $this->_userKey;
	}

	/**
	 * @return string a key used to store user information in session
	 * @since 3.1.1
	 */
	protected function generateUserKey()
	{
		return md5($this->getApplication()->getUniqueID().'prado:user');
	}

	/**
	 * Updates the user data stored in session.
	 * @param IUser user object
	 * @throws new TConfigurationException if session module is not loaded.
	 */
	public function updateSessionUser($user)
	{
		if(!$user->getIsGuest())
		{
			if(($session=$this->getSession())===null)
				throw new TConfigurationException('authmanager_session_required');
			else
				$session->add($this->getUserKey(),$user->saveToString());
		}
	}

	/**
	 * Switches to a new user.
	 * This method will logout the current user first and login with a new one (without password.)
	 * @param string the new username
	 * @return boolean if the switch is successful
	 */
	public function switchUser($username)
	{
		if(($user=$this->_userManager->getUser($username))===null)
			return false;
		$this->updateSessionUser($user);
		$this->getApplication()->setUser($user);
		return true;
	}

	/**
	 * Logs in a user with username and password.
	 * The username and password will be used to validate if login is successful.
	 * If yes, a user object will be created for the application.
	 * @param string username
	 * @param string password
	 * @param integer 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.
	 * @return boolean if login is successful
	 */
	public function login($username,$password,$expire=0)
	{
		if($this->_userManager->validateUser($username,$password))
		{
			if(($user=$this->_userManager->getUser($username))===null)
				return false;
			$this->updateSessionUser($user);
			$this->getApplication()->setUser($user);

			if($expire>0)
			{
				$cookie=new THttpCookie($this->getUserKey(),'');
				$cookie->setExpire(time()+$expire);
				$this->_userManager->saveUserToCookie($cookie);
				$this->getResponse()->getCookies()->add($cookie);
			}
			return true;
		}
		else
			return false;
	}

	/**
	 * Logs out a user.
	 * User session will be destroyed after this method is called.
	 * @throws TConfigurationException if session module is not loaded.
	 */
	public function logout()
	{
		if(($session=$this->getSession())===null)
			throw new TConfigurationException('authmanager_session_required');
		$this->getApplication()->getUser()->setIsGuest(true);
		$session->destroy();
		if($this->getAllowAutoLogin())
		{
			$cookie=new THttpCookie($this->getUserKey(),'');
			$this->getResponse()->getCookies()->add($cookie);
		}
	}
}