PageRenderTime 47ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 1ms

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

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