diff --git a/lib/Cake/Controller/Component/Auth/BaseAuthenticate.php b/lib/Cake/Controller/Component/Auth/BaseAuthenticate.php index 90ba20f1cd1..152c9b61554 100644 --- a/lib/Cake/Controller/Component/Auth/BaseAuthenticate.php +++ b/lib/Cake/Controller/Component/Auth/BaseAuthenticate.php @@ -155,4 +155,15 @@ public function getUser(CakeRequest $request) { return false; } +/** + * Handle unauthenticated access attempt. + * + * @param CakeRequest $request A request object. + * @param CakeResponse $response A response object. + * @return mixed Either true to indicate the unauthenticated request has been + * dealt with and no more action is required by AuthComponent or void (default). + */ + public function unauthenticated(CakeRequest $request, CakeResponse $response) { + } + } diff --git a/lib/Cake/Controller/Component/Auth/BasicAuthenticate.php b/lib/Cake/Controller/Component/Auth/BasicAuthenticate.php index e78eccd1879..d249ed22941 100644 --- a/lib/Cake/Controller/Component/Auth/BasicAuthenticate.php +++ b/lib/Cake/Controller/Component/Auth/BasicAuthenticate.php @@ -82,23 +82,15 @@ public function __construct(ComponentCollection $collection, $settings) { } /** - * Authenticate a user using basic HTTP auth. Will use the configured User model and attempt a - * login using basic HTTP auth. + * Authenticate a user using HTTP auth. Will use the configured User model and attempt a + * login using HTTP auth. * * @param CakeRequest $request The request to authenticate with. * @param CakeResponse $response The response to add headers to. * @return mixed Either false on failure, or an array of user data on success. */ public function authenticate(CakeRequest $request, CakeResponse $response) { - $result = $this->getUser($request); - - if (empty($result)) { - $response->header($this->loginHeaders()); - $response->statusCode(401); - $response->send(); - return false; - } - return $result; + return $this->getUser($request); } /** @@ -117,6 +109,20 @@ public function getUser(CakeRequest $request) { return $this->_findUser($username, $pass); } +/** + * Handles an unauthenticated access attempt by sending appropriate login headers + * + * @param CakeRequest $request A request object. + * @param CakeResponse $response A response object. + * @return boolean True + */ + public function unauthenticated(CakeRequest $request, CakeResponse $response) { + $response->header($this->loginHeaders()); + $response->statusCode(401); + $response->send(); + return true; + } + /** * Generate the login headers * diff --git a/lib/Cake/Controller/Component/Auth/DigestAuthenticate.php b/lib/Cake/Controller/Component/Auth/DigestAuthenticate.php index 60c1345b248..16416fd89dd 100644 --- a/lib/Cake/Controller/Component/Auth/DigestAuthenticate.php +++ b/lib/Cake/Controller/Component/Auth/DigestAuthenticate.php @@ -14,7 +14,7 @@ * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -App::uses('BaseAuthenticate', 'Controller/Component/Auth'); +App::uses('BasicAuthenticate', 'Controller/Component/Auth'); /** * Digest Authentication adapter for AuthComponent. @@ -55,7 +55,7 @@ * @package Cake.Controller.Component.Auth * @since 2.0 */ -class DigestAuthenticate extends BaseAuthenticate { +class DigestAuthenticate extends BasicAuthenticate { /** * Settings for this object. @@ -97,9 +97,6 @@ class DigestAuthenticate extends BaseAuthenticate { */ public function __construct(ComponentCollection $collection, $settings) { parent::__construct($collection, $settings); - if (empty($this->settings['realm'])) { - $this->settings['realm'] = env('SERVER_NAME'); - } if (empty($this->settings['nonce'])) { $this->settings['nonce'] = uniqid(''); } @@ -108,26 +105,6 @@ public function __construct(ComponentCollection $collection, $settings) { } } -/** - * Authenticate a user using Digest HTTP auth. Will use the configured User model and attempt a - * login using Digest HTTP auth. - * - * @param CakeRequest $request The request to authenticate with. - * @param CakeResponse $response The response to add headers to. - * @return mixed Either false on failure, or an array of user data on success. - */ - public function authenticate(CakeRequest $request, CakeResponse $response) { - $user = $this->getUser($request); - - if (empty($user)) { - $response->header($this->loginHeaders()); - $response->statusCode(401); - $response->send(); - return false; - } - return $user; - } - /** * Get a user based on information in the request. Used by cookie-less auth for stateless clients. * diff --git a/lib/Cake/Controller/Component/AuthComponent.php b/lib/Cake/Controller/Component/AuthComponent.php index 443c94aabb4..ccd500a7297 100644 --- a/lib/Cake/Controller/Component/AuthComponent.php +++ b/lib/Cake/Controller/Component/AuthComponent.php @@ -157,8 +157,9 @@ class AuthComponent extends Component { ); /** - * The session key name where the record of the current user is stored. If - * unspecified, it will be "Auth.User". + * The session key name where the record of the current user is stored. Default + * key is "Auth.User". If you are using only stateless authenticators set this + * to false to ensure session is not started. * * @var string */ @@ -188,7 +189,7 @@ class AuthComponent extends Component { * Normally, if a user is redirected to the $loginAction page, the location they * were redirected from will be stored in the session so that they can be * redirected back after a successful login. If this session value is not - * set, the user will be redirected to the page specified in $loginRedirect. + * set, redirectUrl() method will return the url specified in $loginRedirect. * * @var mixed * @link http://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#AuthComponent::$loginRedirect @@ -312,44 +313,43 @@ public function startup(Controller $controller) { /** * Checks whether current action is accessible without authentication. - * If current action is login action referrer url is saved in session which is - * later accessible using AuthComponent::redirectUrl(). * * @param Controller $controller A reference to the instantiating controller object * @return boolean True if action is accessible without authentication else false */ protected function _isAllowed(Controller $controller) { $action = strtolower($controller->request->params['action']); - - $url = ''; - if (isset($controller->request->url)) { - $url = $controller->request->url; - } - $url = Router::normalize($url); - $loginAction = Router::normalize($this->loginAction); - - if ($loginAction != $url && in_array($action, array_map('strtolower', $this->allowedActions))) { - return true; - } - - if ($loginAction == $url) { - if (empty($controller->request->data)) { - if (!$this->Session->check('Auth.redirect') && !$this->loginRedirect && env('HTTP_REFERER')) { - $this->Session->write('Auth.redirect', $controller->referer(null, true)); - } - } + if (in_array($action, array_map('strtolower', $this->allowedActions))) { return true; } return false; } /** - * Handle unauthenticated access attempt. + * Handles unauthenticated access attempt. First the `unathenticated()` method + * of the last authenticator in the chain will be called. The authenticator can + * handle sending response or redirection as appropriate and return `true` to + * indicate no furthur action is necessary. If authenticator returns null this + * method redirects user to login action. If it's an ajax request and + * $ajaxLogin is specified that element is rendered else a 403 http status code + * is returned. * - * @param Controller $controller A reference to the controller object - * @return boolean Returns false + * @param Controller $controller A reference to the controller object. + * @return boolean True if current action is login action else false. */ protected function _unauthenticated(Controller $controller) { + if (empty($this->_authenticateObjects)) { + $this->constructAuthenticate(); + } + $auth = $this->_authenticateObjects[count($this->_authenticateObjects) - 1]; + if ($auth->unauthenticated($this->request, $this->response)) { + return false; + } + + if ($this->_isLoginAction($controller)) { + return true; + } + if (!$controller->request->is('ajax')) { $this->flash($this->authError); $this->Session->write('Auth.redirect', $controller->request->here()); @@ -366,12 +366,40 @@ protected function _unauthenticated(Controller $controller) { return false; } +/** + * Normalizes $loginAction and checks if current request url is same as login + * action. If current url is same as login action, referrer url is saved in session + * which is later accessible using redirectUrl(). + * + * @param Controller $controller A reference to the controller object. + * @return boolean True if current action is login action else false. + */ + protected function _isLoginAction(Controller $controller) { + $url = ''; + if (isset($controller->request->url)) { + $url = $controller->request->url; + } + $url = Router::normalize($url); + $loginAction = Router::normalize($this->loginAction); + + if ($loginAction == $url) { + if (empty($controller->request->data)) { + if (!$this->Session->check('Auth.redirect') && !$this->loginRedirect && env('HTTP_REFERER')) { + $this->Session->write('Auth.redirect', $controller->referer(null, true)); + } + } + return true; + } + return false; + } + /** * Handle unauthorized access attempt * * @param Controller $controller A reference to the controller object * @return boolean Returns false * @throws ForbiddenException + * @see AuthComponent::$unauthorizedRedirect */ protected function _unauthorized(Controller $controller) { if ($this->unauthorizedRedirect === false) { @@ -395,7 +423,7 @@ protected function _unauthorized(Controller $controller) { /** * Attempts to introspect the correct values for object properties. * - * @return boolean + * @return boolean True */ protected function _setDefaults() { $defaults = array( @@ -619,13 +647,12 @@ public function logout() { * @link http://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#accessing-the-logged-in-user */ public static function user($key = null) { - if (empty(self::$_user) && !CakeSession::check(self::$sessionKey)) { - return null; - } if (!empty(self::$_user)) { $user = self::$_user; - } else { + } elseif (self::$sessionKey && CakeSession::check(self::$sessionKey)) { $user = CakeSession::read(self::$sessionKey); + } else { + return null; } if ($key === null) { return $user; @@ -640,10 +667,6 @@ public static function user($key = null) { * @return boolean true if a user can be found, false if one cannot. */ protected function _getUser() { - $user = $this->user(); - if ($user) { - return true; - } if (empty($this->_authenticateObjects)) { $this->constructAuthenticate(); } @@ -654,6 +677,11 @@ protected function _getUser() { return true; } } + + $user = $this->user(); + if ($user) { + return true; + } return false; } diff --git a/lib/Cake/Test/Case/Controller/Component/Auth/BasicAuthenticateTest.php b/lib/Cake/Test/Case/Controller/Component/Auth/BasicAuthenticateTest.php index f11d7d01984..c4410e27862 100644 --- a/lib/Cake/Test/Case/Controller/Component/Auth/BasicAuthenticateTest.php +++ b/lib/Cake/Test/Case/Controller/Component/Auth/BasicAuthenticateTest.php @@ -80,11 +80,10 @@ public function testConstructor() { public function testAuthenticateNoData() { $request = new CakeRequest('posts/index', false); - $this->response->expects($this->once()) - ->method('header') - ->with('WWW-Authenticate: Basic realm="localhost"'); + $this->response->expects($this->never()) + ->method('header'); - $this->assertFalse($this->auth->authenticate($request, $this->response)); + $this->assertFalse($this->auth->getUser($request)); } /** @@ -96,10 +95,6 @@ public function testAuthenticateNoUsername() { $request = new CakeRequest('posts/index', false); $_SERVER['PHP_AUTH_PW'] = 'foobar'; - $this->response->expects($this->once()) - ->method('header') - ->with('WWW-Authenticate: Basic realm="localhost"'); - $this->assertFalse($this->auth->authenticate($request, $this->response)); } @@ -113,10 +108,6 @@ public function testAuthenticateNoPassword() { $_SERVER['PHP_AUTH_USER'] = 'mariano'; $_SERVER['PHP_AUTH_PW'] = null; - $this->response->expects($this->once()) - ->method('header') - ->with('WWW-Authenticate: Basic realm="localhost"'); - $this->assertFalse($this->auth->authenticate($request, $this->response)); } @@ -132,6 +123,8 @@ public function testAuthenticateInjection() { $_SERVER['PHP_AUTH_USER'] = '> 1'; $_SERVER['PHP_AUTH_PW'] = "' OR 1 = 1"; + $this->assertFalse($this->auth->getUser($request)); + $this->assertFalse($this->auth->authenticate($request, $this->response)); } @@ -151,8 +144,8 @@ public function testAuthenticateChallenge() { $this->response->expects($this->at(1)) ->method('send'); - $result = $this->auth->authenticate($request, $this->response); - $this->assertFalse($result); + $result = $this->auth->unauthenticated($request, $this->response); + $this->assertTrue($result); } /** @@ -201,7 +194,7 @@ public function testAuthenticateFailReChallenge() { $this->response->expects($this->at(2)) ->method('send'); - $this->assertFalse($this->auth->authenticate($request, $this->response)); + $this->assertTrue($this->auth->unauthenticated($request, $this->response)); } } diff --git a/lib/Cake/Test/Case/Controller/Component/Auth/DigestAuthenticateTest.php b/lib/Cake/Test/Case/Controller/Component/Auth/DigestAuthenticateTest.php index 80c3447f58f..f07cd719f3b 100644 --- a/lib/Cake/Test/Case/Controller/Component/Auth/DigestAuthenticateTest.php +++ b/lib/Cake/Test/Case/Controller/Component/Auth/DigestAuthenticateTest.php @@ -94,11 +94,10 @@ public function testConstructor() { public function testAuthenticateNoData() { $request = new CakeRequest('posts/index', false); - $this->response->expects($this->once()) - ->method('header') - ->with('WWW-Authenticate: Digest realm="localhost",qop="auth",nonce="123",opaque="123abc"'); + $this->response->expects($this->never()) + ->method('header'); - $this->assertFalse($this->auth->authenticate($request, $this->response)); + $this->assertFalse($this->auth->getUser($request, $this->response)); } /** @@ -133,7 +132,7 @@ public function testAuthenticateWrongUsername() { $this->response->expects($this->at(2)) ->method('send'); - $this->assertFalse($this->auth->authenticate($request, $this->response)); + $this->assertTrue($this->auth->unauthenticated($request, $this->response)); } /** @@ -156,8 +155,8 @@ public function testAuthenticateChallenge() { $this->response->expects($this->at(2)) ->method('send'); - $result = $this->auth->authenticate($request, $this->response); - $this->assertFalse($result); + $result = $this->auth->unauthenticated($request, $this->response); + $this->assertTrue($result); } /** @@ -224,7 +223,7 @@ public function testAuthenticateFailReChallenge() { $this->response->expects($this->at(2)) ->method('send'); - $this->assertFalse($this->auth->authenticate($request, $this->response)); + $this->assertTrue($this->auth->unauthenticated($request, $this->response)); } /** diff --git a/lib/Cake/Test/Case/Controller/Component/AuthComponentTest.php b/lib/Cake/Test/Case/Controller/Component/AuthComponentTest.php index b4e8f9a36c1..d4a6f8badbf 100644 --- a/lib/Cake/Test/Case/Controller/Component/AuthComponentTest.php +++ b/lib/Cake/Test/Case/Controller/Component/AuthComponentTest.php @@ -1348,4 +1348,78 @@ public function testUser() { $result = $this->Auth->user('is_admin'); $this->assertFalse($result); } + +/** + * testStatelessAuthNoRedirect method + * + * @return void + */ + public function testStatelessAuthNoRedirect() { + if (CakeSession::id()) { + session_destroy(); + CakeSession::$id = null; + } + $_SESSION = null; + + AuthComponent::$sessionKey = false; + $this->Auth->authenticate = array('Basic'); + $this->Controller->request['action'] = 'admin_add'; + + $this->Auth->response->expects($this->once()) + ->method('statusCode') + ->with(401); + + $this->Auth->response->expects($this->once()) + ->method('send'); + + $result = $this->Auth->startup($this->Controller); + $this->assertFalse($result); + + $this->assertNull($this->Controller->testUrl); + $this->assertNull(CakeSession::id()); + } + +/** + * testStatelessAuthNoSessionStart method + * + * @return void + */ + public function testStatelessAuthNoSessionStart() { + if (CakeSession::id()) { + session_destroy(); + CakeSession::$id = null; + } + $_SESSION = null; + + $_SERVER['PHP_AUTH_USER'] = 'mariano'; + $_SERVER['PHP_AUTH_PW'] = 'cake'; + + $this->Auth->authenticate = array( + 'Basic' => array('userModel' => 'AuthUser') + ); + $this->Controller->request['action'] = 'admin_add'; + + $result = $this->Auth->startup($this->Controller); + $this->assertTrue($result); + + $this->assertNull(CakeSession::id()); + } + +/** + * testStatelessAuthRedirect method + * + * @return void + */ + public function testStatelessFollowedByStatefulAuth() { + $this->Auth->authenticate = array('Basic', 'Form'); + $this->Controller->request['action'] = 'admin_add'; + + $this->Auth->response->expects($this->never())->method('statusCode'); + $this->Auth->response->expects($this->never())->method('send'); + + $result = $this->Auth->startup($this->Controller); + $this->assertFalse($result); + + $this->assertEquals('/users/login', $this->Controller->testUrl); + } }