1: <?php
2: /**
3: * Authentication component
4: *
5: * Manages user logins and permissions.
6: *
7: * PHP 5
8: *
9: * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
10: * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
11: *
12: * Licensed under The MIT License
13: * For full copyright and license information, please see the LICENSE.txt
14: * Redistributions of files must retain the above copyright notice.
15: *
16: * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
17: * @link http://cakephp.org CakePHP(tm) Project
18: * @package Cake.Controller.Component
19: * @since CakePHP(tm) v 0.10.0.1076
20: * @license http://www.opensource.org/licenses/mit-license.php MIT License
21: */
22:
23: App::uses('Component', 'Controller');
24: App::uses('Router', 'Routing');
25: App::uses('Security', 'Utility');
26: App::uses('Debugger', 'Utility');
27: App::uses('Hash', 'Utility');
28: App::uses('CakeSession', 'Model/Datasource');
29: App::uses('BaseAuthorize', 'Controller/Component/Auth');
30: App::uses('BaseAuthenticate', 'Controller/Component/Auth');
31:
32: /**
33: * Authentication control component class
34: *
35: * Binds access control with user authentication and session management.
36: *
37: * @package Cake.Controller.Component
38: * @link http://book.cakephp.org/2.0/en/core-libraries/components/authentication.html
39: */
40: class AuthComponent extends Component {
41:
42: /**
43: * Constant for 'all'
44: */
45: const ALL = 'all';
46:
47: /**
48: * Other components utilized by AuthComponent
49: *
50: * @var array
51: */
52: public $components = array('Session', 'RequestHandler');
53:
54: /**
55: * An array of authentication objects to use for authenticating users. You can configure
56: * multiple adapters and they will be checked sequentially when users are identified.
57: *
58: * {{{
59: * $this->Auth->authenticate = array(
60: * 'Form' => array(
61: * 'userModel' => 'Users.User'
62: * )
63: * );
64: * }}}
65: *
66: * Using the class name without 'Authenticate' as the key, you can pass in an array of settings for each
67: * authentication object. Additionally you can define settings that should be set to all authentications objects
68: * using the 'all' key:
69: *
70: * {{{
71: * $this->Auth->authenticate = array(
72: * 'all' => array(
73: * 'userModel' => 'Users.User',
74: * 'scope' => array('User.active' => 1)
75: * ),
76: * 'Form',
77: * 'Basic'
78: * );
79: * }}}
80: *
81: * You can also use AuthComponent::ALL instead of the string 'all'.
82: *
83: * @var array
84: * @link http://book.cakephp.org/2.0/en/core-libraries/components/authentication.html
85: */
86: public $authenticate = array('Form');
87:
88: /**
89: * Objects that will be used for authentication checks.
90: *
91: * @var array
92: */
93: protected $_authenticateObjects = array();
94:
95: /**
96: * An array of authorization objects to use for authorizing users. You can configure
97: * multiple adapters and they will be checked sequentially when authorization checks are done.
98: *
99: * {{{
100: * $this->Auth->authorize = array(
101: * 'Crud' => array(
102: * 'actionPath' => 'controllers/'
103: * )
104: * );
105: * }}}
106: *
107: * Using the class name without 'Authorize' as the key, you can pass in an array of settings for each
108: * authorization object. Additionally you can define settings that should be set to all authorization objects
109: * using the 'all' key:
110: *
111: * {{{
112: * $this->Auth->authorize = array(
113: * 'all' => array(
114: * 'actionPath' => 'controllers/'
115: * ),
116: * 'Crud',
117: * 'CustomAuth'
118: * );
119: * }}}
120: *
121: * You can also use AuthComponent::ALL instead of the string 'all'
122: *
123: * @var mixed
124: * @link http://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#authorization
125: */
126: public $authorize = false;
127:
128: /**
129: * Objects that will be used for authorization checks.
130: *
131: * @var array
132: */
133: protected $_authorizeObjects = array();
134:
135: /**
136: * The name of an optional view element to render when an Ajax request is made
137: * with an invalid or expired session
138: *
139: * @var string
140: */
141: public $ajaxLogin = null;
142:
143: /**
144: * Settings to use when Auth needs to do a flash message with SessionComponent::setFlash().
145: * Available keys are:
146: *
147: * - `element` - The element to use, defaults to 'default'.
148: * - `key` - The key to use, defaults to 'auth'
149: * - `params` - The array of additional params to use, defaults to array()
150: *
151: * @var array
152: */
153: public $flash = array(
154: 'element' => 'default',
155: 'key' => 'auth',
156: 'params' => array()
157: );
158:
159: /**
160: * The session key name where the record of the current user is stored. If
161: * unspecified, it will be "Auth.User".
162: *
163: * @var string
164: */
165: public static $sessionKey = 'Auth.User';
166:
167: /**
168: * The current user, used for stateless authentication when
169: * sessions are not available.
170: *
171: * @var array
172: */
173: protected static $_user = array();
174:
175: /**
176: * An URL (defined as a string or array) to the controller action that handles
177: * logins. Defaults to `/users/login`
178: *
179: * @var mixed
180: */
181: public $loginAction = array(
182: 'controller' => 'users',
183: 'action' => 'login',
184: 'plugin' => null
185: );
186:
187: /**
188: * Normally, if a user is redirected to the $loginAction page, the location they
189: * were redirected from will be stored in the session so that they can be
190: * redirected back after a successful login. If this session value is not
191: * set, the user will be redirected to the page specified in $loginRedirect.
192: *
193: * @var mixed
194: * @link http://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#AuthComponent::$loginRedirect
195: */
196: public $loginRedirect = null;
197:
198: /**
199: * The default action to redirect to after the user is logged out. While AuthComponent does
200: * not handle post-logout redirection, a redirect URL will be returned from AuthComponent::logout().
201: * Defaults to AuthComponent::$loginAction.
202: *
203: * @var mixed
204: * @see AuthComponent::$loginAction
205: * @see AuthComponent::logout()
206: */
207: public $logoutRedirect = null;
208:
209: /**
210: * Error to display when user attempts to access an object or action to which they do not have
211: * access.
212: *
213: * @var string
214: * @link http://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#AuthComponent::$authError
215: */
216: public $authError = null;
217:
218: /**
219: * Controls handling of unauthorized access.
220: * - For default value `true` unauthorized user is redirected to the referrer URL
221: * or AuthComponent::$loginRedirect or '/'.
222: * - If set to a string or array the value is used as an URL to redirect to.
223: * - If set to false a ForbiddenException exception is thrown instead of redirecting.
224: *
225: * @var mixed
226: */
227: public $unauthorizedRedirect = true;
228:
229: /**
230: * Controller actions for which user validation is not required.
231: *
232: * @var array
233: * @see AuthComponent::allow()
234: */
235: public $allowedActions = array();
236:
237: /**
238: * Request object
239: *
240: * @var CakeRequest
241: */
242: public $request;
243:
244: /**
245: * Response object
246: *
247: * @var CakeResponse
248: */
249: public $response;
250:
251: /**
252: * Method list for bound controller
253: *
254: * @var array
255: */
256: protected $_methods = array();
257:
258: /**
259: * Initializes AuthComponent for use in the controller
260: *
261: * @param Controller $controller A reference to the instantiating controller object
262: * @return void
263: */
264: public function initialize(Controller $controller) {
265: $this->request = $controller->request;
266: $this->response = $controller->response;
267: $this->_methods = $controller->methods;
268:
269: if (Configure::read('debug') > 0) {
270: Debugger::checkSecurityKeys();
271: }
272: }
273:
274: /**
275: * Main execution method. Handles redirecting of invalid users, and processing
276: * of login form data.
277: *
278: * @param Controller $controller A reference to the instantiating controller object
279: * @return boolean
280: */
281: public function startup(Controller $controller) {
282: $methods = array_flip(array_map('strtolower', $controller->methods));
283: $action = strtolower($controller->request->params['action']);
284:
285: $isMissingAction = (
286: $controller->scaffold === false &&
287: !isset($methods[$action])
288: );
289:
290: if ($isMissingAction) {
291: return true;
292: }
293:
294: if (!$this->_setDefaults()) {
295: return false;
296: }
297: $request = $controller->request;
298:
299: $url = '';
300:
301: if (isset($request->url)) {
302: $url = $request->url;
303: }
304: $url = Router::normalize($url);
305: $loginAction = Router::normalize($this->loginAction);
306:
307: if ($loginAction != $url && in_array($action, array_map('strtolower', $this->allowedActions))) {
308: return true;
309: }
310: if ($loginAction == $url) {
311: if (empty($request->data)) {
312: if (!$this->Session->check('Auth.redirect') && env('HTTP_REFERER')) {
313: $referer = $request->referer(true);
314: $this->Session->write('Auth.redirect', $referer);
315: }
316: }
317: return true;
318: }
319:
320: if (!$this->_getUser()) {
321: if (!$request->is('ajax')) {
322: $this->flash($this->authError);
323: $this->Session->write('Auth.redirect', $request->here(false));
324: $controller->redirect($loginAction);
325: return false;
326: }
327: if (!empty($this->ajaxLogin)) {
328: $controller->viewPath = 'Elements';
329: echo $controller->render($this->ajaxLogin, $this->RequestHandler->ajaxLayout);
330: $this->_stop();
331: return false;
332: }
333: $controller->redirect(null, 403);
334: }
335:
336: if (empty($this->authorize) || $this->isAuthorized($this->user())) {
337: return true;
338: }
339:
340: return $this->_unauthorized($controller);
341: }
342:
343: /**
344: * Handle unauthorized access attempt
345: *
346: * @param Controller $controller A reference to the controller object
347: * @return boolean Returns false
348: * @throws ForbiddenException
349: */
350: protected function _unauthorized(Controller $controller) {
351: if ($this->unauthorizedRedirect === false) {
352: throw new ForbiddenException($this->authError);
353: }
354:
355: $this->flash($this->authError);
356: if ($this->unauthorizedRedirect === true) {
357: $default = '/';
358: if (!empty($this->loginRedirect)) {
359: $default = $this->loginRedirect;
360: }
361: $url = $controller->referer($default, true);
362: } else {
363: $url = $this->unauthorizedRedirect;
364: }
365: $controller->redirect($url, null, true);
366: return false;
367: }
368:
369: /**
370: * Attempts to introspect the correct values for object properties.
371: *
372: * @return boolean
373: */
374: protected function _setDefaults() {
375: $defaults = array(
376: 'logoutRedirect' => $this->loginAction,
377: 'authError' => __d('cake', 'You are not authorized to access that location.')
378: );
379: foreach ($defaults as $key => $value) {
380: if (empty($this->{$key})) {
381: $this->{$key} = $value;
382: }
383: }
384: return true;
385: }
386:
387: /**
388: * Check if the provided user is authorized for the request.
389: *
390: * Uses the configured Authorization adapters to check whether or not a user is authorized.
391: * Each adapter will be checked in sequence, if any of them return true, then the user will
392: * be authorized for the request.
393: *
394: * @param array $user The user to check the authorization of. If empty the user in the session will be used.
395: * @param CakeRequest $request The request to authenticate for. If empty, the current request will be used.
396: * @return boolean True if $user is authorized, otherwise false
397: */
398: public function isAuthorized($user = null, CakeRequest $request = null) {
399: if (empty($user) && !$this->user()) {
400: return false;
401: }
402: if (empty($user)) {
403: $user = $this->user();
404: }
405: if (empty($request)) {
406: $request = $this->request;
407: }
408: if (empty($this->_authorizeObjects)) {
409: $this->constructAuthorize();
410: }
411: foreach ($this->_authorizeObjects as $authorizer) {
412: if ($authorizer->authorize($user, $request) === true) {
413: return true;
414: }
415: }
416: return false;
417: }
418:
419: /**
420: * Loads the authorization objects configured.
421: *
422: * @return mixed Either null when authorize is empty, or the loaded authorization objects.
423: * @throws CakeException
424: */
425: public function constructAuthorize() {
426: if (empty($this->authorize)) {
427: return;
428: }
429: $this->_authorizeObjects = array();
430: $config = Hash::normalize((array)$this->authorize);
431: $global = array();
432: if (isset($config[AuthComponent::ALL])) {
433: $global = $config[AuthComponent::ALL];
434: unset($config[AuthComponent::ALL]);
435: }
436: foreach ($config as $class => $settings) {
437: list($plugin, $class) = pluginSplit($class, true);
438: $className = $class . 'Authorize';
439: App::uses($className, $plugin . 'Controller/Component/Auth');
440: if (!class_exists($className)) {
441: throw new CakeException(__d('cake_dev', 'Authorization adapter "%s" was not found.', $class));
442: }
443: if (!method_exists($className, 'authorize')) {
444: throw new CakeException(__d('cake_dev', 'Authorization objects must implement an authorize method.'));
445: }
446: $settings = array_merge($global, (array)$settings);
447: $this->_authorizeObjects[] = new $className($this->_Collection, $settings);
448: }
449: return $this->_authorizeObjects;
450: }
451:
452: /**
453: * Takes a list of actions in the current controller for which authentication is not required, or
454: * no parameters to allow all actions.
455: *
456: * You can use allow with either an array, or var args.
457: *
458: * `$this->Auth->allow(array('edit', 'add'));` or
459: * `$this->Auth->allow('edit', 'add');` or
460: * `$this->Auth->allow();` to allow all actions
461: *
462: * @param string|array $action,... Controller action name or array of actions
463: * @return void
464: * @link http://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#making-actions-public
465: */
466: public function allow($action = null) {
467: $args = func_get_args();
468: if (empty($args) || $action === null) {
469: $this->allowedActions = $this->_methods;
470: return;
471: }
472: if (isset($args[0]) && is_array($args[0])) {
473: $args = $args[0];
474: }
475: $this->allowedActions = array_merge($this->allowedActions, $args);
476: }
477:
478: /**
479: * Removes items from the list of allowed/no authentication required actions.
480: *
481: * You can use deny with either an array, or var args.
482: *
483: * `$this->Auth->deny(array('edit', 'add'));` or
484: * `$this->Auth->deny('edit', 'add');` or
485: * `$this->Auth->deny();` to remove all items from the allowed list
486: *
487: * @param string|array $action,... Controller action name or array of actions
488: * @return void
489: * @see AuthComponent::allow()
490: * @link http://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#making-actions-require-authorization
491: */
492: public function deny($action = null) {
493: $args = func_get_args();
494: if (empty($args) || $action === null) {
495: $this->allowedActions = array();
496: return;
497: }
498: if (isset($args[0]) && is_array($args[0])) {
499: $args = $args[0];
500: }
501: foreach ($args as $arg) {
502: $i = array_search($arg, $this->allowedActions);
503: if (is_int($i)) {
504: unset($this->allowedActions[$i]);
505: }
506: }
507: $this->allowedActions = array_values($this->allowedActions);
508: }
509:
510: /**
511: * Maps action names to CRUD operations.
512: *
513: * Used for controller-based authentication. Make sure
514: * to configure the authorize property before calling this method. As it delegates $map to all the
515: * attached authorize objects.
516: *
517: * @param array $map Actions to map
518: * @return void
519: * @see BaseAuthorize::mapActions()
520: * @link http://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#mapping-actions-when-using-crudauthorize
521: */
522: public function mapActions($map = array()) {
523: if (empty($this->_authorizeObjects)) {
524: $this->constructAuthorize();
525: }
526: foreach ($this->_authorizeObjects as $auth) {
527: $auth->mapActions($map);
528: }
529: }
530:
531: /**
532: * Log a user in.
533: *
534: * If a $user is provided that data will be stored as the logged in user. If `$user` is empty or not
535: * specified, the request will be used to identify a user. If the identification was successful,
536: * the user record is written to the session key specified in AuthComponent::$sessionKey. Logging in
537: * will also change the session id in order to help mitigate session replays.
538: *
539: * @param array $user Either an array of user data, or null to identify a user using the current request.
540: * @return boolean True on login success, false on failure
541: * @link http://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#identifying-users-and-logging-them-in
542: */
543: public function login($user = null) {
544: $this->_setDefaults();
545:
546: if (empty($user)) {
547: $user = $this->identify($this->request, $this->response);
548: }
549: if ($user) {
550: $this->Session->renew();
551: $this->Session->write(self::$sessionKey, $user);
552: }
553: return $this->loggedIn();
554: }
555:
556: /**
557: * Log a user out.
558: *
559: * Returns the login action to redirect to. Triggers the logout() method of
560: * all the authenticate objects, so they can perform custom logout logic.
561: * AuthComponent will remove the session data, so there is no need to do that
562: * in an authentication object. Logging out will also renew the session id.
563: * This helps mitigate issues with session replays.
564: *
565: * @return string AuthComponent::$logoutRedirect
566: * @see AuthComponent::$logoutRedirect
567: * @link http://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#logging-users-out
568: */
569: public function logout() {
570: $this->_setDefaults();
571: if (empty($this->_authenticateObjects)) {
572: $this->constructAuthenticate();
573: }
574: $user = $this->user();
575: foreach ($this->_authenticateObjects as $auth) {
576: $auth->logout($user);
577: }
578: $this->Session->delete(self::$sessionKey);
579: $this->Session->delete('Auth.redirect');
580: $this->Session->renew();
581: return Router::normalize($this->logoutRedirect);
582: }
583:
584: /**
585: * Get the current user.
586: *
587: * Will prefer the static user cache over sessions. The static user
588: * cache is primarily used for stateless authentication. For stateful authentication,
589: * cookies + sessions will be used.
590: *
591: * @param string $key field to retrieve. Leave null to get entire User record
592: * @return mixed User record. or null if no user is logged in.
593: * @link http://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#accessing-the-logged-in-user
594: */
595: public static function user($key = null) {
596: if (empty(self::$_user) && !CakeSession::check(self::$sessionKey)) {
597: return null;
598: }
599: if (!empty(self::$_user)) {
600: $user = self::$_user;
601: } else {
602: $user = CakeSession::read(self::$sessionKey);
603: }
604: if ($key === null) {
605: return $user;
606: }
607: return Hash::get($user, $key);
608: }
609:
610: /**
611: * Similar to AuthComponent::user() except if the session user cannot be found, connected authentication
612: * objects will have their getUser() methods called. This lets stateless authentication methods function correctly.
613: *
614: * @return boolean true if a user can be found, false if one cannot.
615: */
616: protected function _getUser() {
617: $user = $this->user();
618: if ($user) {
619: return true;
620: }
621: if (empty($this->_authenticateObjects)) {
622: $this->constructAuthenticate();
623: }
624: foreach ($this->_authenticateObjects as $auth) {
625: $result = $auth->getUser($this->request);
626: if (!empty($result) && is_array($result)) {
627: self::$_user = $result;
628: return true;
629: }
630: }
631: return false;
632: }
633:
634: /**
635: * Backwards compatible alias for AuthComponent::redirectUrl()
636: *
637: * @param string|array $url Optional URL to write as the login redirect URL.
638: * @return string Redirect URL
639: * @deprecated 2.3 Use AuthComponent::redirectUrl() instead
640: */
641: public function redirect($url = null) {
642: return $this->redirectUrl($url);
643: }
644:
645: /**
646: * Get the URL a user should be redirected to upon login.
647: *
648: * Pass an URL in to set the destination a user should be redirected to upon
649: * logging in.
650: *
651: * If no parameter is passed, gets the authentication redirect URL. The URL
652: * returned is as per following rules:
653: *
654: * - Returns the normalized URL from session Auth.redirect value if it is
655: * present and for the same domain the current app is running on.
656: * - If there is no session value and there is a $loginRedirect, the $loginRedirect
657: * value is returned.
658: * - If there is no session and no $loginRedirect, / is returned.
659: *
660: * @param string|array $url Optional URL to write as the login redirect URL.
661: * @return string Redirect URL
662: */
663: public function redirectUrl($url = null) {
664: if ($url !== null) {
665: $redir = $url;
666: $this->Session->write('Auth.redirect', $redir);
667: } elseif ($this->Session->check('Auth.redirect')) {
668: $redir = $this->Session->read('Auth.redirect');
669: $this->Session->delete('Auth.redirect');
670:
671: if (Router::normalize($redir) == Router::normalize($this->loginAction)) {
672: $redir = $this->loginRedirect;
673: }
674: } elseif ($this->loginRedirect) {
675: $redir = $this->loginRedirect;
676: } else {
677: $redir = '/';
678: }
679: if (is_array($redir)) {
680: return Router::url($redir + array('base' => false));
681: }
682: return $redir;
683: }
684:
685: /**
686: * Use the configured authentication adapters, and attempt to identify the user
687: * by credentials contained in $request.
688: *
689: * @param CakeRequest $request The request that contains authentication data.
690: * @param CakeResponse $response The response
691: * @return array User record data, or false, if the user could not be identified.
692: */
693: public function identify(CakeRequest $request, CakeResponse $response) {
694: if (empty($this->_authenticateObjects)) {
695: $this->constructAuthenticate();
696: }
697: foreach ($this->_authenticateObjects as $auth) {
698: $result = $auth->authenticate($request, $response);
699: if (!empty($result) && is_array($result)) {
700: return $result;
701: }
702: }
703: return false;
704: }
705:
706: /**
707: * loads the configured authentication objects.
708: *
709: * @return mixed either null on empty authenticate value, or an array of loaded objects.
710: * @throws CakeException
711: */
712: public function constructAuthenticate() {
713: if (empty($this->authenticate)) {
714: return;
715: }
716: $this->_authenticateObjects = array();
717: $config = Hash::normalize((array)$this->authenticate);
718: $global = array();
719: if (isset($config[AuthComponent::ALL])) {
720: $global = $config[AuthComponent::ALL];
721: unset($config[AuthComponent::ALL]);
722: }
723: foreach ($config as $class => $settings) {
724: list($plugin, $class) = pluginSplit($class, true);
725: $className = $class . 'Authenticate';
726: App::uses($className, $plugin . 'Controller/Component/Auth');
727: if (!class_exists($className)) {
728: throw new CakeException(__d('cake_dev', 'Authentication adapter "%s" was not found.', $class));
729: }
730: if (!method_exists($className, 'authenticate')) {
731: throw new CakeException(__d('cake_dev', 'Authentication objects must implement an authenticate method.'));
732: }
733: $settings = array_merge($global, (array)$settings);
734: $this->_authenticateObjects[] = new $className($this->_Collection, $settings);
735: }
736: return $this->_authenticateObjects;
737: }
738:
739: /**
740: * Hash a password with the application's salt value (as defined with Configure::write('Security.salt');
741: *
742: * This method is intended as a convenience wrapper for Security::hash(). If you want to use
743: * a hashing/encryption system not supported by that method, do not use this method.
744: *
745: * @param string $password Password to hash
746: * @return string Hashed password
747: * @link http://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#hashing-passwords
748: */
749: public static function password($password) {
750: return Security::hash($password, null, true);
751: }
752:
753: /**
754: * Component shutdown. If user is logged in, wipe out redirect.
755: *
756: * @param Controller $controller Instantiating controller
757: * @return void
758: */
759: public function shutdown(Controller $controller) {
760: if ($this->loggedIn()) {
761: $this->Session->delete('Auth.redirect');
762: }
763: }
764:
765: /**
766: * Check whether or not the current user has data in the session, and is considered logged in.
767: *
768: * @return boolean true if the user is logged in, false otherwise
769: */
770: public function loggedIn() {
771: return (boolean)$this->user();
772: }
773:
774: /**
775: * Set a flash message. Uses the Session component, and values from AuthComponent::$flash.
776: *
777: * @param string $message The message to set.
778: * @return void
779: */
780: public function flash($message) {
781: $this->Session->setFlash($message, $this->flash['element'], $this->flash['params'], $this->flash['key']);
782: }
783:
784: }
785: