ProxyTest.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. <?php
  2. /**
  3. * Zend Framework
  4. *
  5. * LICENSE
  6. *
  7. * This source file is subject to the new BSD license that is bundled
  8. * with this package in the file LICENSE.txt.
  9. * It is also available through the world-wide-web at this URL:
  10. * http://framework.zend.com/license/new-bsd
  11. * If you did not receive a copy of the license and are unable to
  12. * obtain it through the world-wide-web, please send an email
  13. * to license@zend.com so we can send you a copy immediately.
  14. *
  15. * @category Zend
  16. * @package Zend_Auth
  17. * @subpackage UnitTests
  18. * @copyright Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
  19. * @license http://framework.zend.com/license/new-bsd New BSD License
  20. * @version $Id$
  21. */
  22. /**
  23. * Test helper
  24. */
  25. require_once dirname(__FILE__) . '/../../../../TestHelper.php';
  26. /**
  27. * @see Zend_Auth_Adapter_Http
  28. */
  29. require_once 'Zend/Auth/Adapter/Http.php';
  30. /**
  31. * @see Zend_Auth_Adapter_Http_Resolver_File
  32. */
  33. require_once 'Zend/Auth/Adapter/Http/Resolver/File.php';
  34. /**
  35. * @see Zend_Controller_Request_Http
  36. */
  37. require_once 'Zend/Controller/Request/Http.php';
  38. /**
  39. * @see Zend_Controller_Response_Http
  40. */
  41. require_once 'Zend/Controller/Response/Http.php';
  42. /**
  43. * @category Zend
  44. * @package Zend_Auth
  45. * @subpackage UnitTests
  46. * @copyright Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
  47. * @license http://framework.zend.com/license/new-bsd New BSD License
  48. * @group Zend_Auth
  49. */
  50. class Zend_Auth_Adapter_Http_ProxyTest extends PHPUnit_Framework_TestCase
  51. {
  52. /**
  53. * Path to test files
  54. *
  55. * @var string
  56. */
  57. protected $_filesPath;
  58. /**
  59. * HTTP Basic configuration
  60. *
  61. * @var array
  62. */
  63. protected $_basicConfig;
  64. /**
  65. * HTTP Digest configuration
  66. *
  67. * @var array
  68. */
  69. protected $_digestConfig;
  70. /**
  71. * HTTP Basic Digest configuration
  72. *
  73. * @var array
  74. */
  75. protected $_bothConfig;
  76. /**
  77. * File resolver setup against with HTTP Basic auth file
  78. *
  79. * @var Zend_Auth_Adapter_Http_Resolver_File
  80. */
  81. protected $_basicResolver;
  82. /**
  83. * File resolver setup against with HTTP Digest auth file
  84. *
  85. * @var Zend_Auth_Adapter_Http_Resolver_File
  86. */
  87. protected $_digestResolver;
  88. /**
  89. * Sets up test configuration
  90. *
  91. * @return void
  92. */
  93. public function __construct()
  94. {
  95. $this->_filesPath = dirname(__FILE__) . '/_files';
  96. $this->_basicResolver = new Zend_Auth_Adapter_Http_Resolver_File("{$this->_filesPath}/htbasic.1");
  97. $this->_digestResolver = new Zend_Auth_Adapter_Http_Resolver_File("{$this->_filesPath}/htdigest.3");
  98. $this->_basicConfig = array(
  99. 'accept_schemes' => 'basic',
  100. 'realm' => 'Test Realm',
  101. 'proxy_auth' => true
  102. );
  103. $this->_digestConfig = array(
  104. 'accept_schemes' => 'digest',
  105. 'realm' => 'Test Realm',
  106. 'digest_domains' => '/ http://localhost/',
  107. 'nonce_timeout' => 300,
  108. 'proxy_auth' => true
  109. );
  110. $this->_bothConfig = array(
  111. 'accept_schemes' => 'basic digest',
  112. 'realm' => 'Test Realm',
  113. 'digest_domains' => '/ http://localhost/',
  114. 'nonce_timeout' => 300,
  115. 'proxy_auth' => true
  116. );
  117. }
  118. public function testBasicChallenge()
  119. {
  120. // Trying to authenticate without sending an Proxy-Authorization header
  121. // should result in a 407 reply with a Proxy-Authenticate header, and a
  122. // false result.
  123. // The expected Basic Proxy-Authenticate header value
  124. $basic = 'Basic realm="' . $this->_bothConfig['realm'] . '"';
  125. $data = $this->_doAuth('', 'basic');
  126. $this->_checkUnauthorized($data, $basic);
  127. }
  128. public function testDigestChallenge()
  129. {
  130. // Trying to authenticate without sending an Proxy-Authorization header
  131. // should result in a 407 reply with a Proxy-Authenticate header, and a
  132. // false result.
  133. // The expected Digest Proxy-Authenticate header value
  134. $digest = $this->_digestChallenge();
  135. $data = $this->_doAuth('', 'digest');
  136. $this->_checkUnauthorized($data, $digest);
  137. }
  138. public function testBothChallenges()
  139. {
  140. // Trying to authenticate without sending an Proxy-Authorization header
  141. // should result in a 407 reply with at least one Proxy-Authenticate
  142. // header, and a false result.
  143. $data = $this->_doAuth('', 'both');
  144. extract($data); // $result, $status, $headers
  145. // The expected Proxy-Authenticate header values
  146. $basic = 'Basic realm="' . $this->_bothConfig['realm'] . '"';
  147. $digest = $this->_digestChallenge();
  148. // Make sure the result is false
  149. $this->assertType('Zend_Auth_Result', $result);
  150. $this->assertFalse($result->isValid());
  151. // Verify the status code and the presence of both challenges
  152. $this->assertEquals(407, $status);
  153. $this->assertEquals('Proxy-Authenticate', $headers[0]['name']);
  154. $this->assertEquals('Proxy-Authenticate', $headers[1]['name']);
  155. // Check to see if the expected challenges match the actual
  156. $this->assertEquals($basic, $headers[0]['value']);
  157. $this->assertEquals($digest, $headers[1]['value']);
  158. }
  159. public function testBasicAuthValidCreds()
  160. {
  161. // Attempt Basic Authentication with a valid username and password
  162. $data = $this->_doAuth('Basic ' . base64_encode('Bryce:ThisIsNotMyPassword'), 'basic');
  163. $this->_checkOK($data);
  164. }
  165. public function testBasicAuthBadCreds()
  166. {
  167. // Ensure that credentials containing invalid characters are treated as
  168. // a bad username or password.
  169. // The expected Basic WWW-Authenticate header value
  170. $basic = 'Basic realm="' . $this->_basicConfig['realm'] . '"';
  171. $data = $this->_doAuth('Basic ' . base64_encode("Bad\tChars:In:Creds"), 'basic');
  172. $this->_checkUnauthorized($data, $basic);
  173. }
  174. public function testBasicAuthBadUser()
  175. {
  176. // Attempt Basic Authentication with a bad username and password
  177. // The expected Basic Proxy-Authenticate header value
  178. $basic = 'Basic realm="' . $this->_basicConfig['realm'] . '"';
  179. $data = $this->_doAuth('Basic ' . base64_encode('Nobody:NotValid'), 'basic');
  180. $this->_checkUnauthorized($data, $basic);
  181. }
  182. public function testBasicAuthBadPassword()
  183. {
  184. // Attempt Basic Authentication with a valid username, but invalid
  185. // password
  186. // The expected Basic WWW-Authenticate header value
  187. $basic = 'Basic realm="' . $this->_basicConfig['realm'] . '"';
  188. $data = $this->_doAuth('Basic ' . base64_encode('Bryce:Invalid'), 'basic');
  189. $this->_checkUnauthorized($data, $basic);
  190. }
  191. public function testDigestAuthValidCreds()
  192. {
  193. // Attempt Digest Authentication with a valid username and password
  194. $data = $this->_doAuth($this->_digestReply('Bryce', 'ThisIsNotMyPassword'), 'digest');
  195. $this->_checkOK($data);
  196. }
  197. public function testDigestAuthDefaultAlgo()
  198. {
  199. // If the client omits the aglorithm argument, it should default to MD5,
  200. // and work just as above
  201. $cauth = $this->_digestReply('Bryce', 'ThisIsNotMyPassword');
  202. $cauth = preg_replace('/algorithm="MD5", /', '', $cauth);
  203. $data = $this->_doAuth($cauth, 'digest');
  204. $this->_checkOK($data);
  205. }
  206. public function testDigestAuthQuotedNC()
  207. {
  208. // The nonce count isn't supposed to be quoted, but apparently some
  209. // clients do anyway.
  210. $cauth = $this->_digestReply('Bryce', 'ThisIsNotMyPassword');
  211. $cauth = preg_replace('/nc=00000001/', 'nc="00000001"', $cauth);
  212. $data = $this->_doAuth($cauth, 'digest');
  213. $this->_checkOK($data);
  214. }
  215. public function testDigestAuthBadCreds()
  216. {
  217. // Attempt Digest Authentication with a bad username and password
  218. // The expected Digest Proxy-Authenticate header value
  219. $digest = $this->_digestChallenge();
  220. $data = $this->_doAuth($this->_digestReply('Nobody', 'NotValid'), 'digest');
  221. $this->_checkUnauthorized($data, $digest);
  222. }
  223. public function testDigestTampered()
  224. {
  225. // Create the tampered header value
  226. $tampered = $this->_digestReply('Bryce', 'ThisIsNotMyPassword');
  227. $tampered = preg_replace(
  228. '/ nonce="[a-fA-F0-9]{32}", /',
  229. ' nonce="' . str_repeat('0', 32).'", ',
  230. $tampered
  231. );
  232. // The expected Digest Proxy-Authenticate header value
  233. $digest = $this->_digestChallenge();
  234. $data = $this->_doAuth($tampered, 'digest');
  235. $this->_checkUnauthorized($data, $digest);
  236. }
  237. public function testBadSchemeRequest()
  238. {
  239. // Sending a request for an invalid authentication scheme should result
  240. // in a 400 Bad Request response.
  241. $data = $this->_doAuth('Invalid ' . base64_encode('Nobody:NotValid'), 'basic');
  242. $this->_checkBadRequest($data);
  243. }
  244. public function testBadDigestRequest()
  245. {
  246. // If any of the individual parts of the Digest Proxy-Authorization header
  247. // are bad, it results in a 400 Bad Request. But that's a lot of
  248. // possibilities, so we're just going to pick one for now.
  249. $bad = $this->_digestReply('Bryce', 'ThisIsNotMyPassword');
  250. $bad = preg_replace(
  251. '/realm="([^"]+)"/', // cut out the realm
  252. '', $bad
  253. );
  254. $data = $this->_doAuth($bad, 'digest');
  255. $this->_checkBadRequest($data);
  256. }
  257. /**
  258. * Acts like a client sending the given Authenticate header value.
  259. *
  260. * @param string $clientHeader Authenticate header value
  261. * @param string $scheme Which authentication scheme to use
  262. * @return array Containing the result, the response headers, and the status
  263. */
  264. public function _doAuth($clientHeader, $scheme)
  265. {
  266. // Set up stub request and response objects
  267. $request = $this->getMock('Zend_Controller_Request_Http');
  268. $response = new Zend_Controller_Response_Http;
  269. $response->setHttpResponseCode(200);
  270. $response->headersSentThrowsException = false;
  271. // Set stub method return values
  272. $request->expects($this->any())
  273. ->method('getRequestUri')
  274. ->will($this->returnValue('/'));
  275. $request->expects($this->any())
  276. ->method('getMethod')
  277. ->will($this->returnValue('GET'));
  278. $request->expects($this->any())
  279. ->method('getServer')
  280. ->will($this->returnValue('PHPUnit'));
  281. $request->expects($this->any())
  282. ->method('getHeader')
  283. ->will($this->returnValue($clientHeader));
  284. // Select an Authentication scheme
  285. switch ($scheme) {
  286. case 'basic':
  287. $use = $this->_basicConfig;
  288. break;
  289. case 'digest':
  290. $use = $this->_digestConfig;
  291. break;
  292. case 'both':
  293. default:
  294. $use = $this->_bothConfig;
  295. }
  296. // Create the HTTP Auth adapter
  297. $a = new Zend_Auth_Adapter_Http($use);
  298. $a->setBasicResolver($this->_basicResolver);
  299. $a->setDigestResolver($this->_digestResolver);
  300. // Send the authentication request
  301. $a->setRequest($request);
  302. $a->setResponse($response);
  303. $result = $a->authenticate();
  304. $return = array(
  305. 'result' => $result,
  306. 'status' => $response->getHttpResponseCode(),
  307. 'headers' => $response->getHeaders()
  308. );
  309. return $return;
  310. }
  311. /**
  312. * Constructs a local version of the digest challenge we expect to receive
  313. *
  314. * @return string
  315. */
  316. protected function _digestChallenge()
  317. {
  318. $timeout = ceil(time() / 300) * 300;
  319. $nonce = md5($timeout . ':PHPUnit:Zend_Auth_Adapter_Http');
  320. $opaque = md5('Opaque Data:Zend_Auth_Adapter_Http');
  321. $wwwauth = 'Digest '
  322. . 'realm="' . $this->_digestConfig['realm'] . '", '
  323. . 'domain="' . $this->_digestConfig['digest_domains'] . '", '
  324. . 'nonce="' . $nonce . '", '
  325. . 'opaque="' . $opaque . '", '
  326. . 'algorithm="MD5", '
  327. . 'qop="auth"';
  328. return $wwwauth;
  329. }
  330. /**
  331. * Constructs a client digest Proxy-Authorization header
  332. *
  333. * @param string $user
  334. * @param string $pass
  335. * @return string
  336. */
  337. protected function _digestReply($user, $pass)
  338. {
  339. $nc = '00000001';
  340. $timeout = ceil(time() / 300) * 300;
  341. $nonce = md5($timeout . ':PHPUnit:Zend_Auth_Adapter_Http');
  342. $opaque = md5('Opaque Data:Zend_Auth_Adapter_Http');
  343. $cnonce = md5('cnonce');
  344. $response = md5(md5($user . ':' . $this->_digestConfig['realm'] . ':' . $pass) . ":$nonce:$nc:$cnonce:auth:"
  345. . md5('GET:/'));
  346. $cauth = 'Digest '
  347. . 'username="Bryce", '
  348. . 'realm="' . $this->_digestConfig['realm'] . '", '
  349. . 'nonce="' . $nonce . '", '
  350. . 'uri="/", '
  351. . 'response="' . $response . '", '
  352. . 'algorithm="MD5", '
  353. . 'cnonce="' . $cnonce . '", '
  354. . 'opaque="' . $opaque . '", '
  355. . 'qop="auth", '
  356. . 'nc=' . $nc;
  357. return $cauth;
  358. }
  359. /**
  360. * Checks for an expected 407 Proxy-Unauthorized response
  361. *
  362. * @param array $data Authentication results
  363. * @param string $expected Expected Proxy-Authenticate header value
  364. * @return void
  365. */
  366. protected function _checkUnauthorized($data, $expected)
  367. {
  368. extract($data); // $result, $status, $headers
  369. // Make sure the result is false
  370. $this->assertType('Zend_Auth_Result', $result);
  371. $this->assertFalse($result->isValid());
  372. // Verify the status code and the presence of the challenge
  373. $this->assertEquals(407, $status);
  374. $this->assertEquals('Proxy-Authenticate', $headers[0]['name']);
  375. // Check to see if the expected challenge matches the actual
  376. $this->assertEquals($expected, $headers[0]['value']);
  377. }
  378. /**
  379. * Checks for an expected 200 OK response
  380. *
  381. * @param array $data Authentication results
  382. * @return void
  383. */
  384. protected function _checkOK($data)
  385. {
  386. extract($data); // $result, $status, $headers
  387. // Make sure the result is true
  388. $this->assertType('Zend_Auth_Result', $result);
  389. $this->assertTrue($result->isValid());
  390. // Verify we got a 200 response
  391. $this->assertEquals(200, $status);
  392. }
  393. /**
  394. * Checks for an expected 400 Bad Request response
  395. *
  396. * @param array $data Authentication results
  397. * @return void
  398. */
  399. protected function _checkBadRequest($data)
  400. {
  401. extract($data); // $result, $status, $headers
  402. // Make sure the result is false
  403. $this->assertType('Zend_Auth_Result', $result);
  404. $this->assertFalse($result->isValid());
  405. // Make sure it set the right HTTP code
  406. $this->assertEquals(400, $status);
  407. }
  408. }