diff --git a/cake/libs/controller/components/auth/basic_authenticate.php b/cake/libs/controller/components/auth/basic_authenticate.php index af1361bd1..085b0770c 100644 --- a/cake/libs/controller/components/auth/basic_authenticate.php +++ b/cake/libs/controller/components/auth/basic_authenticate.php @@ -13,15 +13,88 @@ * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ App::import('Component', 'auth/base_authenticate'); +App::import('Core', 'String'); + class BasicAuthenticate extends BaseAuthenticate { +/** + * Settings for this object. + * + * - `fields` The fields to use to identify a user by. + * - `userModel` The model name of the User, defaults to User. + * - `scope` Additional conditions to use when looking up and authenticating users, + * i.e. `array('User.is_active' => 1).` + * + * @var array + */ + public $settings = array( + 'fields' => array( + 'username' => 'username', + 'password' => 'password' + ), + 'userModel' => 'User', + 'scope' => array(), + 'realm' => '', + ); + +/** + * Constructor, completes configuration for basic authentication. + * + * @return void + */ + public function __construct($settings) { + parent::__construct($settings); + if (empty($this->settings['realm'])) { + $this->settings['realm'] = env('SERVER_NAME'); + } + } /** * Authenticate a user using basic HTTP auth. Will use the configured User model and attempt a * login using basic 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) { - + public function authenticate(CakeRequest $request, CakeResponse $response) { + $username = env('PHP_AUTH_USER'); + $pass = env('PHP_AUTH_PW'); + if (empty($username) || empty($pass)) { + $response->header($this->loginHeaders()); + return false; + } + $userModel = $this->settings['userModel']; + list($plugin, $model) = pluginSplit($userModel); + $fields = $this->settings['fields']; + + $conditions = array( + $model . '.' . $fields['username'] => $username, + $model . '.' . $fields['password'] => $this->hash($pass), + ); + if (!empty($this->settings['scope'])) { + $conditions = array_merge($conditions, $this->settings['scope']); + } + $result = ClassRegistry::init($userModel)->find('first', array( + 'conditions' => $conditions, + 'recursive' => 0 + )); + if (empty($result) || empty($result[$model])) { + $response->header($this->loginHeaders()); + $response->header('Location', Router::reverse($request)); + $response->statusCode(401); + $response->send(); + return false; + } + unset($result[$model][$fields['password']]); + return $result[$model]; + } + +/** + * Generate the login headers + * + * @return string Headers for logging in. + */ + public function loginHeaders() { + return sprintf('WWW-Authenticate: Basic realm="%s"', $this->settings['realm']); } } \ No newline at end of file diff --git a/cake/tests/cases/libs/controller/components/auth/basic_authenticate.test.php b/cake/tests/cases/libs/controller/components/auth/basic_authenticate.test.php index 7da7e692b..867b50c11 100644 --- a/cake/tests/cases/libs/controller/components/auth/basic_authenticate.test.php +++ b/cake/tests/cases/libs/controller/components/auth/basic_authenticate.test.php @@ -17,6 +17,8 @@ App::import('Component', 'auth/basic_authenticate'); App::import('Model', 'AppModel'); App::import('Core', 'CakeRequest'); +App::import('Core', 'CakeResponse'); + require_once CAKE_TESTS . 'cases' . DS . 'libs' . DS . 'model' . DS . 'models.php'; @@ -38,10 +40,24 @@ class BasicAuthenticateTest extends CakeTestCase { parent::setUp(); $this->auth = new BasicAuthenticate(array( 'fields' => array('username' => 'user', 'password' => 'password'), - 'userModel' => 'User' + 'userModel' => 'User', + 'realm' => 'localhost', )); + $password = Security::hash('password', null, true); ClassRegistry::init('User')->updateAll(array('password' => '"' . $password . '"')); + $this->server = $_SERVER; + $this->response = $this->getMock('CakeResponse'); + } + +/** + * teardown + * + * @return void + */ + function tearDown() { + parent::tearDown(); + $_SERVER = $this->server; } /** @@ -56,6 +72,7 @@ class BasicAuthenticateTest extends CakeTestCase { )); $this->assertEquals('AuthUser', $object->settings['userModel']); $this->assertEquals(array('username' => 'user', 'password' => 'password'), $object->settings['fields']); + $this->assertEquals(env('SERVER_NAME'), $object->settings['realm']); } /** @@ -65,8 +82,12 @@ class BasicAuthenticateTest extends CakeTestCase { */ function testAuthenticateNoData() { $request = new CakeRequest('posts/index', false); - $request->data = array(); - $this->assertFalse($this->auth->authenticate($request)); + + $this->response->expects($this->once()) + ->method('header') + ->with('WWW-Authenticate: Basic realm="localhost"'); + + $this->assertFalse($this->auth->authenticate($request, $this->response)); } /** @@ -76,8 +97,13 @@ class BasicAuthenticateTest extends CakeTestCase { */ function testAuthenticateNoUsername() { $request = new CakeRequest('posts/index', false); - $request->data = array('User' => array('password' => 'foobar')); - $this->assertFalse($this->auth->authenticate($request)); + $_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)); } /** @@ -87,8 +113,13 @@ class BasicAuthenticateTest extends CakeTestCase { */ function testAuthenticateNoPassword() { $request = new CakeRequest('posts/index', false); - $request->data = array('User' => array('user' => 'mariano')); - $this->assertFalse($this->auth->authenticate($request)); + $_SERVER['PHP_AUTH_USER'] = 'mariano'; + + $this->response->expects($this->once()) + ->method('header') + ->with('WWW-Authenticate: Basic realm="localhost"'); + + $this->assertFalse($this->auth->authenticate($request, $this->response)); } /** @@ -98,14 +129,30 @@ class BasicAuthenticateTest extends CakeTestCase { */ function testAuthenticateInjection() { $request = new CakeRequest('posts/index', false); - $request->data = array( - 'User' => array( - 'user' => '> 1', - 'password' => "' OR 1 = 1" - )); - $this->assertFalse($this->auth->authenticate($request)); + $request->addParams(array('pass' => array(), 'named' => array())); + + $_SERVER['PHP_AUTH_USER'] = '> 1'; + $_SERVER['PHP_AUTH_PW'] = "' OR 1 = 1"; + + $this->assertFalse($this->auth->authenticate($request, $this->response)); } +/** + * test that challenge headers are sent when no credentials are found. + * + * @return void + */ + function testAuthenticateChallenge() { + $request = new CakeRequest('posts/index', false); + $request->addParams(array('pass' => array(), 'named' => array())); + + $this->response->expects($this->once()) + ->method('header') + ->with('WWW-Authenticate: Basic realm="localhost"'); + + $result = $this->auth->authenticate($request, $this->response); + $this->assertFalse($result); + } /** * test authenticate sucesss * @@ -113,11 +160,12 @@ class BasicAuthenticateTest extends CakeTestCase { */ function testAuthenticateSuccess() { $request = new CakeRequest('posts/index', false); - $request->data = array('User' => array( - 'user' => 'mariano', - 'password' => 'password' - )); - $result = $this->auth->authenticate($request); + $request->addParams(array('pass' => array(), 'named' => array())); + + $_SERVER['PHP_AUTH_USER'] = 'mariano'; + $_SERVER['PHP_AUTH_PW'] = 'password'; + + $result = $this->auth->authenticate($request, $this->response); $expected = array( 'id' => 1, 'user' => 'mariano', @@ -132,15 +180,30 @@ class BasicAuthenticateTest extends CakeTestCase { * * @return void */ - function testAuthenticateScopeFail() { + function testAuthenticateFailReChallenge() { $this->auth->settings['scope'] = array('user' => 'nate'); $request = new CakeRequest('posts/index', false); - $request->data = array('User' => array( - 'user' => 'mariano', - 'password' => 'password' - )); + $request->addParams(array('pass' => array(), 'named' => array())); - $this->assertFalse($this->auth->authenticate($request)); + $_SERVER['PHP_AUTH_USER'] = 'mariano'; + $_SERVER['PHP_AUTH_PW'] = 'password'; + + $this->response->expects($this->at(0)) + ->method('header') + ->with('WWW-Authenticate: Basic realm="localhost"'); + + $this->response->expects($this->at(1)) + ->method('header') + ->with('Location', Router::reverse($request)); + + $this->response->expects($this->at(2)) + ->method('statusCode') + ->with(401); + + $this->response->expects($this->at(3)) + ->method('send'); + + $this->assertFalse($this->auth->authenticate($request, $this->response)); } } \ No newline at end of file