PageRenderTime 44ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/tests/Zend/Authentication/Adapter/Http/AuthTest.php

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