PageRenderTime 58ms CodeModel.GetById 22ms RepoModel.GetById 1ms app.codeStats 1ms

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

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