From 226eb616d1993cf78f32408b20369e05cdcd9019 Mon Sep 17 00:00:00 2001 From: mark_story Date: Sat, 29 Dec 2012 21:57:18 -0500 Subject: [PATCH] Initial implementation of Digest authentication Most ported from the existing DigestAuthentication adapter for the old HttpSocket. It still needs some real world testing as I'm not completely confident in it yet. Change authentication strategy construction so it gets the current client. This makes it possible to do challenge requests which are needed for digest auth. --- lib/Cake/Network/Http/Auth/Digest.php | 137 ++++++++++++++++++ lib/Cake/Network/Http/Client.php | 2 +- .../TestCase/Network/Http/Auth/DigestTest.php | 124 ++++++++++++++++ 3 files changed, 262 insertions(+), 1 deletion(-) create mode 100644 lib/Cake/Network/Http/Auth/Digest.php create mode 100644 lib/Cake/Test/TestCase/Network/Http/Auth/DigestTest.php diff --git a/lib/Cake/Network/Http/Auth/Digest.php b/lib/Cake/Network/Http/Auth/Digest.php new file mode 100644 index 00000000000..26ac127dc4f --- /dev/null +++ b/lib/Cake/Network/Http/Auth/Digest.php @@ -0,0 +1,137 @@ +_client = $client; + } + +/** + * Add Authorization header to the request. + * + * @param Request $request + * @param array $credentials + * @return void + * @see http://www.ietf.org/rfc/rfc2617.txt + */ + public function authentication(Request $request, $credentials) { + if (!isset($credentials['username'], $credentials['password'])) { + return; + } + if (!isset($credentials['realm'])) { + $credentials = $this->_getServerInfo($request, $credentials); + } + if (!isset($credentials['realm'])) { + return; + } + $value = $this->_generateHeader($request, $credentials); + $request->header('Authorization', $value); + } + +/** + * Retrieve information about the authentication + * + * Will get the realm and other tokens by performing + * another request without authentication to get authentication + * challenge. + * + * @param Request $request + * @param array $credentials + * @return Array modified credentials. + */ + protected function _getServerInfo(Request $request, $credentials) { + $response = $this->_client->get( + $request->url(), + [], + ['auth' => []] + ); + + if (!$response->header('WWW-Authenticate')) { + return false; + } + preg_match_all( + '@(\w+)=(?:(?:")([^"]+)"|([^\s,$]+))@', + $response->header('WWW-Authenticate'), + $matches, + PREG_SET_ORDER + ); + foreach ($matches as $match) { + $credentials[$match[1]] = $match[2]; + } + if (!empty($credentials['qop']) && empty($credentials['nc'])) { + $credentials['nc'] = 1; + } + return $credentials; + } + +/** + * Generate the header Authorization + * + * @param Request $request + * @param array $credentials + * @return string + */ + protected function _generateHeader(Request $request, $credentials) { + $path = parse_url($request->url(), PHP_URL_PATH); + $a1 = md5($credentials['username'] . ':' . $credentials['realm'] . ':' . $credentials['password']); + $a2 = md5($request->method() . ':' . $path); + + if (empty($credentials['qop'])) { + $response = md5($a1 . ':' . $credentials['nonce'] . ':' . $a2); + } else { + $credentials['cnonce'] = uniqid(); + $nc = sprintf('%08x', $credentials['nc']++); + $response = md5($a1 . ':' . $credentials['nonce'] . ':' . $nc . ':' . $credentials['cnonce'] . ':auth:' . $a2); + } + + $authHeader = 'Digest '; + $authHeader .= 'username="' . str_replace(array('\\', '"'), array('\\\\', '\\"'), $credentials['username']) . '", '; + $authHeader .= 'realm="' . $credentials['realm'] . '", '; + $authHeader .= 'nonce="' . $credentials['nonce'] . '", '; + $authHeader .= 'uri="' . $path . '", '; + $authHeader .= 'response="' . $response . '"'; + if (!empty($credentials['opaque'])) { + $authHeader .= ', opaque="' . $credentials['opaque'] . '"'; + } + if (!empty($credentials['qop'])) { + $authHeader .= ', qop="auth", nc=' . $nc . ', cnonce="' . $credentials['cnonce'] . '"'; + } + return $authHeader; + } + +} diff --git a/lib/Cake/Network/Http/Client.php b/lib/Cake/Network/Http/Client.php index 9a0e3400711..089a9050d3d 100644 --- a/lib/Cake/Network/Http/Client.php +++ b/lib/Cake/Network/Http/Client.php @@ -429,7 +429,7 @@ protected function _createAuth($auth, $options) { __d('cake_dev', 'Invalid authentication type %s', $name) ); } - return new $class($options); + return new $class($this, $options); } } diff --git a/lib/Cake/Test/TestCase/Network/Http/Auth/DigestTest.php b/lib/Cake/Test/TestCase/Network/Http/Auth/DigestTest.php new file mode 100644 index 00000000000..d5675ac01c6 --- /dev/null +++ b/lib/Cake/Test/TestCase/Network/Http/Auth/DigestTest.php @@ -0,0 +1,124 @@ +client = $this->getMock( + 'Cake\Network\Http\Client', + ['send'] + ); + $this->auth = new Digest($this->client); + } + +/** + * test getting data from additional request method + * + * @return void + */ + public function testRealmAndNonceFromExtraRequest() { + $headers = [ + 'WWW-Authenticate: Digest realm="The batcave",nonce="4cded326c6c51"' + ]; + + $response = new Response($headers, ''); + $this->client->expects($this->once()) + ->method('send') + ->will($this->returnValue($response)); + + $auth = ['username' => 'admin', 'password' => '1234']; + $request = (new Request())->method(Request::METHOD_GET) + ->url('http://example.com/some/path'); + + $this->auth->authentication($request, $auth); + + $result = $request->header('Authorization'); + $this->assertContains('Digest', $result); + $this->assertContains('realm="The batcave"', $result); + $this->assertContains('nonce="4cded326c6c51"', $result); + $this->assertContains('response="a21a874c0b29165929f5d24d1aad2c47"', $result); + $this->assertContains('uri="/some/path"', $result); + $this->assertNotContains('qop=', $result); + $this->assertNotContains('nc=', $result); + } + +/** + * testQop method + * + * @return void + */ + public function testQop() { + $headers = [ + 'WWW-Authenticate: Digest realm="The batcave",nonce="4cded326c6c51",qop="auth"' + ]; + + $response = new Response($headers, ''); + $this->client->expects($this->once()) + ->method('send') + ->will($this->returnValue($response)); + + $auth = ['username' => 'admin', 'password' => '1234']; + $request = (new Request())->method(Request::METHOD_GET) + ->url('http://example.com/some/path'); + + $this->auth->authentication($request, $auth); + $result = $request->header('Authorization'); + + $this->assertContains('qop="auth"', $result); + $this->assertContains('nc=00000001', $result); + $this->assertRegexp('/cnonce="[a-z0-9]+"/', $result); + } + +/** + * testOpaque method + * + * @return void + */ + public function testOpaque() { + $headers = [ + 'WWW-Authenticate: Digest realm="The batcave",nonce="4cded326c6c51",opaque="d8ea7aa61a1693024c4cc3a516f49b3c"' + ]; + + $response = new Response($headers, ''); + $this->client->expects($this->once()) + ->method('send') + ->will($this->returnValue($response)); + + $auth = ['username' => 'admin', 'password' => '1234']; + $request = (new Request())->method(Request::METHOD_GET) + ->url('http://example.com/some/path'); + + $this->auth->authentication($request, $auth); + $result = $request->header('Authorization'); + + $this->assertContains('opaque="d8ea7aa61a1693024c4cc3a516f49b3c"', $result); + } + +}