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); + } + +}