From 945e49ad09816c79892fdddbf9fe313317316fd9 Mon Sep 17 00:00:00 2001 From: mark_story Date: Sat, 22 Jan 2011 13:29:56 -0500 Subject: [PATCH] Starting Digest auth, using Basic auth as a starting base. --- .../components/auth/digest_authenticate.php | 108 +++++++++ .../auth/digest_authenticate.test.php | 216 ++++++++++++++++++ 2 files changed, 324 insertions(+) create mode 100644 cake/libs/controller/components/auth/digest_authenticate.php create mode 100644 cake/tests/cases/libs/controller/components/auth/digest_authenticate.test.php diff --git a/cake/libs/controller/components/auth/digest_authenticate.php b/cake/libs/controller/components/auth/digest_authenticate.php new file mode 100644 index 000000000..f58623315 --- /dev/null +++ b/cake/libs/controller/components/auth/digest_authenticate.php @@ -0,0 +1,108 @@ + 1).` + * + * @var array + */ + public $settings = array( + 'fields' => array( + 'username' => 'username', + 'password' => 'password' + ), + 'userModel' => 'User', + 'scope' => array(), + 'realm' => '', + 'qop' => 'auth', + 'nonce' => '', + 'opaque' => '' + ); + +/** + * Constructor, completes configuration for digest authentication. + * + * @return void + */ + public function __construct($settings) { + parent::__construct($settings); + if (empty($this->settings['realm'])) { + $this->settings['realm'] = env('SERVER_NAME'); + } + if (empty($this->settings['nonce'])) { + $this->settings['realm'] = uniqid(''); + } + if (empty($this->settings['opaque'])) { + $this->settings['opaque'] = md5($this->settings['realm']); + } + } +/** + * 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) { + $username = env('PHP_AUTH_USER'); + $pass = env('PHP_AUTH_PW'); + + if (empty($username) || empty($pass)) { + $response->header($this->loginHeaders()); + $response->send(); + return false; + } + + $result = $this->_findUser($username, $pass); + + if (empty($result)) { + $response->header($this->loginHeaders()); + $response->header('Location', Router::reverse($request)); + $response->statusCode(401); + $response->send(); + return false; + } + return $result; + } + +/** + * Generate the login headers + * + * @return string Headers for logging in. + */ + public function loginHeaders() { + $options = array( + 'realm' => $this->settings['realm'], + 'qop' => $this->settings['qop'], + 'nonce' => $this->settings['nonce'], + 'opaque' => $this->settings['opaque'] + ); + $opts = array(); + foreach ($options as $k => $v) { + $opts[] = sprintf('%s="%s"', $k, $v); + } + return 'WWW-Authenticate: Digest ' . implode(',', $opts); + } +} \ No newline at end of file diff --git a/cake/tests/cases/libs/controller/components/auth/digest_authenticate.test.php b/cake/tests/cases/libs/controller/components/auth/digest_authenticate.test.php new file mode 100644 index 000000000..40a5a6b71 --- /dev/null +++ b/cake/tests/cases/libs/controller/components/auth/digest_authenticate.test.php @@ -0,0 +1,216 @@ +auth = new DigestAuthenticate(array( + 'fields' => array('username' => 'user', 'password' => 'password'), + 'userModel' => 'User', + 'realm' => 'localhost', + 'nonce' => 123, + 'opaque' => '123abc' + )); + + $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; + } + +/** + * test applying settings in the constructor + * + * @return void + */ + function testConstructor() { + $object = new DigestAuthenticate(array( + 'userModel' => 'AuthUser', + 'fields' => array('username' => 'user', 'password' => 'password'), + 'nonce' => 123456 + )); + $this->assertEquals('AuthUser', $object->settings['userModel']); + $this->assertEquals(array('username' => 'user', 'password' => 'password'), $object->settings['fields']); + $this->assertEquals(123456, $object->settings['nonce']); + $this->assertEquals(env('SERVER_NAME'), $object->settings['realm']); + } + +/** + * test the authenticate method + * + * @return void + */ + 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->assertFalse($this->auth->authenticate($request, $this->response)); + } + +/** + * test the authenticate method + * + * @return void + */ + function testAuthenticateNoUsername() { + $request = new CakeRequest('posts/index', false); + $_SERVER['PHP_AUTH_PW'] = 'foobar'; + + $this->response->expects($this->once()) + ->method('header') + ->with('WWW-Authenticate: Digest realm="localhost",qop="auth",nonce="123",opaque="123abc"'); + + $this->assertFalse($this->auth->authenticate($request, $this->response)); + } + +/** + * test the authenticate method + * + * @return void + */ + function testAuthenticateNoPassword() { + $request = new CakeRequest('posts/index', false); + $_SERVER['PHP_AUTH_USER'] = 'mariano'; + + $this->response->expects($this->once()) + ->method('header') + ->with('WWW-Authenticate: Digest realm="localhost",qop="auth",nonce="123",opaque="123abc"'); + + $this->assertFalse($this->auth->authenticate($request, $this->response)); + } + +/** + * test the authenticate method + * + * @return void + */ + function testAuthenticateInjection() { + $request = new CakeRequest('posts/index', false); + $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->at(0)) + ->method('header') + ->with('WWW-Authenticate: Digest realm="localhost",qop="auth",nonce="123",opaque="123abc"'); + + $this->response->expects($this->at(1)) + ->method('send'); + + $result = $this->auth->authenticate($request, $this->response); + $this->assertFalse($result); + } +/** + * test authenticate sucesss + * + * @return void + */ + function testAuthenticateSuccess() { + $request = new CakeRequest('posts/index', false); + $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', + 'created' => '2007-03-17 01:16:23', + 'updated' => '2007-03-17 01:18:31' + ); + $this->assertEquals($expected, $result); + } + +/** + * test scope failure. + * + * @return void + */ + function testAuthenticateFailReChallenge() { + $this->auth->settings['scope'] = array('user' => 'nate'); + $request = new CakeRequest('posts/index', false); + $request->addParams(array('pass' => array(), 'named' => array())); + + $_SERVER['PHP_AUTH_USER'] = 'mariano'; + $_SERVER['PHP_AUTH_PW'] = 'password'; + + $this->response->expects($this->at(0)) + ->method('header') + ->with('WWW-Authenticate: Digest realm="localhost",qop="auth",nonce="123",opaque="123abc"'); + + $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