AuthTest.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  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-2010 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. * PHPUnit_Framework_TestCase
  24. */
  25. require_once 'PHPUnit/Framework/TestCase.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-2010 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_AuthTest 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. * Set 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. );
  102. $this->_digestConfig = array(
  103. 'accept_schemes' => 'digest',
  104. 'realm' => 'Test Realm',
  105. 'digest_domains' => '/ http://localhost/',
  106. 'nonce_timeout' => 300
  107. );
  108. $this->_bothConfig = array(
  109. 'accept_schemes' => 'basic digest',
  110. 'realm' => 'Test Realm',
  111. 'digest_domains' => '/ http://localhost/',
  112. 'nonce_timeout' => 300
  113. );
  114. }
  115. public function testBasicChallenge()
  116. {
  117. // Trying to authenticate without sending an Authorization header
  118. // should result in a 401 reply with a Www-Authenticate header, and a
  119. // false result.
  120. // The expected Basic Www-Authenticate header value
  121. $basic = 'Basic realm="' . $this->_bothConfig['realm'] . '"';
  122. $data = $this->_doAuth('', 'basic');
  123. $this->_checkUnauthorized($data, $basic);
  124. }
  125. public function testDigestChallenge()
  126. {
  127. // Trying to authenticate without sending an Authorization header
  128. // should result in a 401 reply with a Www-Authenticate header, and a
  129. // false result.
  130. // The expected Digest Www-Authenticate header value
  131. $digest = $this->_digestChallenge();
  132. $data = $this->_doAuth('', 'digest');
  133. $this->_checkUnauthorized($data, $digest);
  134. }
  135. public function testBothChallenges()
  136. {
  137. // Trying to authenticate without sending an Authorization header
  138. // should result in a 401 reply with at least one Www-Authenticate
  139. // header, and a false result.
  140. $data = $this->_doAuth('', 'both');
  141. extract($data); // $result, $status, $headers
  142. // The expected Www-Authenticate header values
  143. $basic = 'Basic realm="' . $this->_bothConfig['realm'] . '"';
  144. $digest = $this->_digestChallenge();
  145. // Make sure the result is false
  146. $this->assertType('Zend_Auth_Result', $result);
  147. $this->assertFalse($result->isValid());
  148. // Verify the status code and the presence of both challenges
  149. $this->assertEquals(401, $status);
  150. $this->assertEquals('Www-Authenticate', $headers[0]['name']);
  151. $this->assertEquals('Www-Authenticate', $headers[1]['name']);
  152. // Check to see if the expected challenges match the actual
  153. $this->assertEquals($basic, $headers[0]['value']);
  154. $this->assertEquals($digest, $headers[1]['value']);
  155. }
  156. public function testBasicAuthValidCreds()
  157. {
  158. // Attempt Basic Authentication with a valid username and password
  159. $data = $this->_doAuth('Basic ' . base64_encode('Bryce:ThisIsNotMyPassword'), 'basic');
  160. $this->_checkOK($data);
  161. }
  162. public function testBasicAuthBadCreds()
  163. {
  164. // Ensure that credentials containing invalid characters are treated as
  165. // a bad username or password.
  166. // The expected Basic Www-Authenticate header value
  167. $basic = 'Basic realm="' . $this->_basicConfig['realm'] . '"';
  168. $data = $this->_doAuth('Basic ' . base64_encode("Bad\tChars:In:Creds"), 'basic');
  169. $this->_checkUnauthorized($data, $basic);
  170. }
  171. public function testBasicAuthBadUser()
  172. {
  173. // Attempt Basic Authentication with a nonexistant username and
  174. // password
  175. // The expected Basic Www-Authenticate header value
  176. $basic = 'Basic realm="' . $this->_basicConfig['realm'] . '"';
  177. $data = $this->_doAuth('Basic ' . base64_encode('Nobody:NotValid'), 'basic');
  178. $this->_checkUnauthorized($data, $basic);
  179. }
  180. public function testBasicAuthBadPassword()
  181. {
  182. // Attempt Basic Authentication with a valid username, but invalid
  183. // password
  184. // The expected Basic Www-Authenticate header value
  185. $basic = 'Basic realm="' . $this->_basicConfig['realm'] . '"';
  186. $data = $this->_doAuth('Basic ' . base64_encode('Bryce:Invalid'), 'basic');
  187. $this->_checkUnauthorized($data, $basic);
  188. }
  189. public function testDigestAuthValidCreds()
  190. {
  191. // Attempt Digest Authentication with a valid username and password
  192. $data = $this->_doAuth($this->_digestReply('Bryce', 'ThisIsNotMyPassword'), 'digest');
  193. $this->_checkOK($data);
  194. }
  195. public function testDigestAuthDefaultAlgo()
  196. {
  197. // If the client omits the aglorithm argument, it should default to MD5,
  198. // and work just as above
  199. $cauth = $this->_digestReply('Bryce', 'ThisIsNotMyPassword');
  200. $cauth = preg_replace('/algorithm="MD5", /', '', $cauth);
  201. $data = $this->_doAuth($cauth, 'digest');
  202. $this->_checkOK($data);
  203. }
  204. public function testDigestAuthQuotedNC()
  205. {
  206. // The nonce count isn't supposed to be quoted, but apparently some
  207. // clients do anyway.
  208. $cauth = $this->_digestReply('Bryce', 'ThisIsNotMyPassword');
  209. $cauth = preg_replace('/nc=00000001/', 'nc="00000001"', $cauth);
  210. $data = $this->_doAuth($cauth, 'digest');
  211. $this->_checkOK($data);
  212. }
  213. public function testDigestAuthBadCreds()
  214. {
  215. // Attempt Digest Authentication with a bad username and password
  216. // The expected Digest Www-Authenticate header value
  217. $digest = $this->_digestChallenge();
  218. $data = $this->_doAuth($this->_digestReply('Nobody', 'NotValid'), 'digest');
  219. $this->_checkUnauthorized($data, $digest);
  220. }
  221. public function testDigestAuthBadCreds2()
  222. {
  223. // Formerly, a username with invalid characters would result in a 400
  224. // response, but now should result in 401 response.
  225. // The expected Digest Www-Authenticate header value
  226. $digest = $this->_digestChallenge();
  227. $data = $this->_doAuth($this->_digestReply('Bad:chars', 'NotValid'), 'digest');
  228. $this->_checkUnauthorized($data, $digest);
  229. }
  230. public function testDigestTampered()
  231. {
  232. // Create the tampered header value
  233. $tampered = $this->_digestReply('Bryce', 'ThisIsNotMyPassword');
  234. $tampered = preg_replace(
  235. '/ nonce="[a-fA-F0-9]{32}", /',
  236. ' nonce="'.str_repeat('0', 32).'", ',
  237. $tampered
  238. );
  239. // The expected Digest Www-Authenticate header value
  240. $digest = $this->_digestChallenge();
  241. $data = $this->_doAuth($tampered, 'digest');
  242. $this->_checkUnauthorized($data, $digest);
  243. }
  244. public function testBadSchemeRequest()
  245. {
  246. // Sending a request for an invalid authentication scheme should result
  247. // in a 400 Bad Request response.
  248. $data = $this->_doAuth('Invalid ' . base64_encode('Nobody:NotValid'), 'basic');
  249. $this->_checkBadRequest($data);
  250. }
  251. public function testBadDigestRequest()
  252. {
  253. // If any of the individual parts of the Digest Authorization header
  254. // are bad, it results in a 400 Bad Request. But that's a lot of
  255. // possibilities, so we're just going to pick one for now.
  256. $bad = $this->_digestReply('Bryce', 'ThisIsNotMyPassword');
  257. $bad = preg_replace(
  258. '/realm="([^"]+)"/', // cut out the realm
  259. '', $bad
  260. );
  261. $data = $this->_doAuth($bad, 'digest');
  262. $this->_checkBadRequest($data);
  263. }
  264. /**
  265. * Acts like a client sending the given Authenticate header value.
  266. *
  267. * @param string $clientHeader Authenticate header value
  268. * @param string $scheme Which authentication scheme to use
  269. * @return array Containing the result, response headers, and the status
  270. */
  271. protected function _doAuth($clientHeader, $scheme)
  272. {
  273. // Set up stub request and response objects
  274. $request = $this->getMock('Zend_Controller_Request_Http');
  275. $response = new Zend_Controller_Response_Http;
  276. $response->setHttpResponseCode(200);
  277. $response->headersSentThrowsException = false;
  278. // Set stub method return values
  279. $request->expects($this->any())
  280. ->method('getRequestUri')
  281. ->will($this->returnValue('/'));
  282. $request->expects($this->any())
  283. ->method('getMethod')
  284. ->will($this->returnValue('GET'));
  285. $request->expects($this->any())
  286. ->method('getServer')
  287. ->will($this->returnValue('PHPUnit'));
  288. $request->expects($this->any())
  289. ->method('getHeader')
  290. ->will($this->returnValue($clientHeader));
  291. // Select an Authentication scheme
  292. switch ($scheme) {
  293. case 'basic':
  294. $use = $this->_basicConfig;
  295. break;
  296. case 'digest':
  297. $use = $this->_digestConfig;
  298. break;
  299. case 'both':
  300. default:
  301. $use = $this->_bothConfig;
  302. }
  303. // Create the HTTP Auth adapter
  304. $a = new Zend_Auth_Adapter_Http($use);
  305. $a->setBasicResolver($this->_basicResolver);
  306. $a->setDigestResolver($this->_digestResolver);
  307. // Send the authentication request
  308. $a->setRequest($request);
  309. $a->setResponse($response);
  310. $result = $a->authenticate();
  311. $return = array(
  312. 'result' => $result,
  313. 'status' => $response->getHttpResponseCode(),
  314. 'headers' => $response->getHeaders()
  315. );
  316. return $return;
  317. }
  318. /**
  319. * Constructs a local version of the digest challenge we expect to receive
  320. *
  321. * @return string
  322. */
  323. protected function _digestChallenge()
  324. {
  325. $timeout = ceil(time() / 300) * 300;
  326. $nonce = md5($timeout . ':PHPUnit:Zend_Auth_Adapter_Http');
  327. $opaque = md5('Opaque Data:Zend_Auth_Adapter_Http');
  328. $wwwauth = 'Digest '
  329. . 'realm="' . $this->_digestConfig['realm'] . '", '
  330. . 'domain="' . $this->_digestConfig['digest_domains'] . '", '
  331. . 'nonce="' . $nonce . '", '
  332. . 'opaque="' . $opaque . '", '
  333. . 'algorithm="MD5", '
  334. . 'qop="auth"';
  335. return $wwwauth;
  336. }
  337. /**
  338. * Constructs a client digest Authorization header
  339. *
  340. * @return string
  341. */
  342. protected function _digestReply($user, $pass)
  343. {
  344. $nc = '00000001';
  345. $timeout = ceil(time() / 300) * 300;
  346. $nonce = md5($timeout . ':PHPUnit:Zend_Auth_Adapter_Http');
  347. $opaque = md5('Opaque Data:Zend_Auth_Adapter_Http');
  348. $cnonce = md5('cnonce');
  349. $response = md5(md5($user . ':' . $this->_digestConfig['realm'] . ':' . $pass) . ":$nonce:$nc:$cnonce:auth:"
  350. . md5('GET:/'));
  351. $cauth = 'Digest '
  352. . 'username="Bryce", '
  353. . 'realm="' . $this->_digestConfig['realm'] . '", '
  354. . 'nonce="' . $nonce . '", '
  355. . 'uri="/", '
  356. . 'response="' . $response . '", '
  357. . 'algorithm="MD5", '
  358. . 'cnonce="' . $cnonce . '", '
  359. . 'opaque="' . $opaque . '", '
  360. . 'qop="auth", '
  361. . 'nc=' . $nc;
  362. return $cauth;
  363. }
  364. /**
  365. * Checks for an expected 401 Unauthorized response
  366. *
  367. * @param array $data Authentication results
  368. * @param string $expected Expected Www-Authenticate header value
  369. * @return void
  370. */
  371. protected function _checkUnauthorized($data, $expected)
  372. {
  373. extract($data); // $result, $status, $headers
  374. // Make sure the result is false
  375. $this->assertType('Zend_Auth_Result', $result);
  376. $this->assertFalse($result->isValid());
  377. // Verify the status code and the presence of the challenge
  378. $this->assertEquals(401, $status);
  379. $this->assertEquals('Www-Authenticate', $headers[0]['name']);
  380. // Check to see if the expected challenge matches the actual
  381. $this->assertEquals($expected, $headers[0]['value']);
  382. }
  383. /**
  384. * Checks for an expected 200 OK response
  385. *
  386. * @param array $data Authentication results
  387. * @return void
  388. */
  389. protected function _checkOK($data)
  390. {
  391. extract($data); // $result, $status, $headers
  392. // Make sure the result is true
  393. $this->assertType('Zend_Auth_Result', $result);
  394. $this->assertTrue($result->isValid());
  395. // Verify we got a 200 response
  396. $this->assertEquals(200, $status);
  397. }
  398. /**
  399. * Checks for an expected 400 Bad Request response
  400. *
  401. * @param array $data Authentication results
  402. * @return void
  403. */
  404. protected function _checkBadRequest($data)
  405. {
  406. extract($data); // $result, $status, $headers
  407. // Make sure the result is false
  408. $this->assertType('Zend_Auth_Result', $result);
  409. $this->assertFalse($result->isValid());
  410. // Make sure it set the right HTTP code
  411. $this->assertEquals(400, $status);
  412. }
  413. }