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

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

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