FormAuthTest.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. <?php
  2. declare(strict_types=1);
  3. final class FormAuthTest extends \PHPUnit\Framework\TestCase {
  4. public function testCheck(): void {
  5. $password = '1234567';
  6. $ok = FreshRSS_FormAuth::passwordRequirementsMet($password);
  7. self::assertTrue($ok);
  8. }
  9. public function testReqsCheckReturnsFalseIfEmpty(): void {
  10. $password = '';
  11. $ok = FreshRSS_FormAuth::passwordRequirementsMet($password);
  12. self::assertFalse($ok);
  13. }
  14. public function testReqsCheckReturnsFalseIfLessThan7Characters(): void {
  15. $password = '123456';
  16. $ok = FreshRSS_FormAuth::passwordRequirementsMet($password);
  17. self::assertFalse($ok);
  18. }
  19. public function testReqsCheckReturnsFalseIfLessThan7Utf8Bytes(): void {
  20. $password = str_repeat('é', 3);
  21. self::assertSame(6, strlen($password));
  22. $ok = FreshRSS_FormAuth::passwordRequirementsMet($password);
  23. self::assertFalse($ok);
  24. }
  25. public function testReqsCheckReturnsTrueIfAtExactly72Characters(): void {
  26. $password = str_repeat('A', 72);
  27. $ok = FreshRSS_FormAuth::passwordRequirementsMet($password);
  28. self::assertTrue($ok);
  29. }
  30. public function testReqsCheckReturnsFalseIfMoreThan72Characters(): void {
  31. $password = str_repeat('A', 73);
  32. $ok = FreshRSS_FormAuth::passwordRequirementsMet($password);
  33. self::assertFalse($ok);
  34. }
  35. public function testReqsCheckReturnsTrueIfAtExactly72Utf8Bytes(): void {
  36. $password = str_repeat('é', 36);
  37. self::assertSame(72, strlen($password));
  38. $ok = FreshRSS_FormAuth::passwordRequirementsMet($password);
  39. self::assertTrue($ok);
  40. }
  41. public function testReqsCheckReturnsFalseIfMoreThan72Utf8Bytes(): void {
  42. $password = str_repeat('é', 37);
  43. self::assertSame(74, strlen($password));
  44. $ok = FreshRSS_FormAuth::passwordRequirementsMet($password);
  45. self::assertFalse($ok);
  46. }
  47. public function testReqsCheckReturnsFalseIfAtExactly73Utf8Bytes(): void {
  48. $password = str_repeat('é', 36) . 'a';
  49. self::assertSame(73, strlen($password));
  50. $ok = FreshRSS_FormAuth::passwordRequirementsMet($password);
  51. self::assertFalse($ok);
  52. }
  53. public function testReqsCheckAcceptsOtherMultibyteBoundary(): void {
  54. $password = str_repeat('€', 24);
  55. self::assertSame(72, strlen($password));
  56. self::assertTrue(FreshRSS_FormAuth::passwordRequirementsMet($password));
  57. $password .= '€';
  58. self::assertSame(75, strlen($password));
  59. self::assertFalse(FreshRSS_FormAuth::passwordRequirementsMet($password));
  60. }
  61. public function testReqsCheckReturnsTrueIfLessThan7CharactersAndWithNoMinLengthEnforcement(): void {
  62. $password = '123456';
  63. $ok = FreshRSS_FormAuth::passwordRequirementsMet($password, ['enforceMinLength' => false]);
  64. self::assertTrue($ok);
  65. }
  66. public function testReqsCheckReturnsTrueIfLessThan7Utf8BytesAndWithNoMinLengthEnforcement(): void {
  67. $password = str_repeat('é', 3);
  68. self::assertSame(6, strlen($password));
  69. $ok = FreshRSS_FormAuth::passwordRequirementsMet($password, ['enforceMinLength' => false]);
  70. self::assertTrue($ok);
  71. }
  72. public function testAuthWithInvalidUsernameAndCorrectCredentialsFail(): void {
  73. $username = ':-)';
  74. $password = '1234567';
  75. $hash = FreshRSS_password_Util::hash($password);
  76. $ok = FreshRSS_FormAuth::checkCredentials(
  77. $username, $hash, $password
  78. );
  79. self::assertFalse($ok);
  80. self::assertSame("Invalid credential parameters: user={$username}", Minz_Log::getLastLog());
  81. }
  82. public function testAuthWithValidUsernameAndWrongCredentialsFail(): void {
  83. $username = 'admin';
  84. $password = 'correctpassword';
  85. $hash = FreshRSS_password_Util::hash('badpassword');
  86. $ok = FreshRSS_FormAuth::checkCredentials(
  87. $username, $hash, $password
  88. );
  89. self::assertFalse($ok);
  90. self::assertSame('', Minz_Log::getLastLog());
  91. }
  92. public function testAuthWithValidUsernameAndCorrectCredentialsOk(): void {
  93. $username = 'admin';
  94. $password = 'correctpassword';
  95. $hash = FreshRSS_password_Util::hash($password);
  96. $ok = FreshRSS_FormAuth::checkCredentials(
  97. $username, $hash, $password
  98. );
  99. self::assertTrue($ok);
  100. self::assertSame('', Minz_Log::getLastLog());
  101. }
  102. public function testAuthWithValidUsernameAndCorrectCredentialsAndTooLongUtf8PasswordOk(): void {
  103. $username = 'admin';
  104. $password = str_repeat('a', 71) . 'é';
  105. self::assertSame(73, strlen($password));
  106. $hash = FreshRSS_password_Util::hash($password);
  107. $ok = FreshRSS_FormAuth::checkCredentials(
  108. $username, $hash, $password
  109. );
  110. self::assertTrue($ok); // Login is successful with a cut-off UTF-8 character at the end
  111. self::assertSame('', Minz_Log::getLastLog());
  112. $password .= 'é';
  113. $hash = FreshRSS_password_Util::hash($password);
  114. $ok = FreshRSS_FormAuth::checkCredentials(
  115. $username, $hash, $password
  116. );
  117. self::assertFalse($ok);
  118. self::assertSame("Exceeded maximum allowed password length during authentication: user={$username}", Minz_Log::getLastLog());
  119. }
  120. public function testAuthWithValidUsernameAndCorrectCredentialsAndTooLongUtf8PasswordWithCutOffCharacterFollowedByAnotherCharacterFail(): void {
  121. $username = 'admin';
  122. $password = str_repeat('a', 71) . 'éa';
  123. self::assertSame(74, strlen($password));
  124. $hash = FreshRSS_password_Util::hash($password);
  125. $ok = FreshRSS_FormAuth::checkCredentials(
  126. $username, $hash, $password
  127. );
  128. self::assertFalse($ok);
  129. self::assertSame("Exceeded maximum allowed password length during authentication: user={$username}", Minz_Log::getLastLog());
  130. }
  131. public function testAuthWithValidUsernameAndCorrectCredentialsAndTooLongUtf8PasswordWithThreeByteCharacterOk(): void {
  132. $username = 'admin';
  133. $password = str_repeat('a', 71) . '€';
  134. self::assertSame(74, strlen($password));
  135. $hash = FreshRSS_password_Util::hash($password);
  136. $ok = FreshRSS_FormAuth::checkCredentials(
  137. $username, $hash, $password
  138. );
  139. self::assertTrue($ok); // Login is successful with a cut-off UTF-8 character at the end
  140. self::assertSame('', Minz_Log::getLastLog());
  141. $password .= '€';
  142. $hash = FreshRSS_password_Util::hash($password);
  143. $ok = FreshRSS_FormAuth::checkCredentials(
  144. $username, $hash, $password
  145. );
  146. self::assertFalse($ok);
  147. self::assertSame("Exceeded maximum allowed password length during authentication: user={$username}", Minz_Log::getLastLog());
  148. }
  149. public function testAuthWithValidUsernameAndCorrectCredentialsAndTooLongUtf8PasswordWithFourByteCharacterOk(): void {
  150. $username = 'admin';
  151. $password = str_repeat('a', 71) . '😀';
  152. self::assertSame(75, strlen($password));
  153. $hash = FreshRSS_password_Util::hash($password);
  154. $ok = FreshRSS_FormAuth::checkCredentials(
  155. $username, $hash, $password
  156. );
  157. self::assertTrue($ok); // Login is successful with a cut-off UTF-8 character at the end
  158. self::assertSame('', Minz_Log::getLastLog());
  159. $password .= '😀';
  160. $hash = FreshRSS_password_Util::hash($password);
  161. $ok = FreshRSS_FormAuth::checkCredentials(
  162. $username, $hash, $password
  163. );
  164. self::assertFalse($ok);
  165. self::assertSame("Exceeded maximum allowed password length during authentication: user={$username}", Minz_Log::getLastLog());
  166. }
  167. public function testAuthWithValidUsernameAndCorrectCredentialsAndTooLongUtf8PasswordWithThreeByteCharacterCutOffAfterTwoBytesOk(): void {
  168. $username = 'admin';
  169. $password = str_repeat('a', 70) . '€';
  170. self::assertSame(73, strlen($password));
  171. $hash = FreshRSS_password_Util::hash($password);
  172. $ok = FreshRSS_FormAuth::checkCredentials(
  173. $username, $hash, $password
  174. );
  175. self::assertTrue($ok);
  176. self::assertSame('', Minz_Log::getLastLog());
  177. }
  178. public function testAuthWithValidUsernameAndCorrectCredentialsAndTooLongUtf8PasswordWithFourByteCharacterCutOffAfterTwoBytesOk(): void {
  179. $username = 'admin';
  180. $password = str_repeat('a', 70) . '😀';
  181. self::assertSame(74, strlen($password));
  182. $hash = FreshRSS_password_Util::hash($password);
  183. $ok = FreshRSS_FormAuth::checkCredentials(
  184. $username, $hash, $password
  185. );
  186. self::assertTrue($ok);
  187. self::assertSame('', Minz_Log::getLastLog());
  188. }
  189. public function testAuthWithValidUsernameAndCorrectCredentialsAndTooLongUtf8PasswordWithFourByteCharacterCutOffAfterThreeBytesOk(): void {
  190. $username = 'admin';
  191. $password = str_repeat('a', 69) . '😀';
  192. self::assertSame(73, strlen($password));
  193. $hash = FreshRSS_password_Util::hash($password);
  194. $ok = FreshRSS_FormAuth::checkCredentials(
  195. $username, $hash, $password
  196. );
  197. self::assertTrue($ok);
  198. self::assertSame('', Minz_Log::getLastLog());
  199. }
  200. public function testAuthWithValidUsernameAndCorrectCredentialsAndTooLongUtf8PasswordWithoutCutOffCharacter(): void {
  201. $username = 'admin';
  202. $password = str_repeat('a', 72) . 'é';
  203. self::assertSame(74, strlen($password));
  204. $hash = FreshRSS_password_Util::hash($password);
  205. $ok = FreshRSS_FormAuth::checkCredentials(
  206. $username, $hash, $password
  207. );
  208. self::assertFalse($ok);
  209. self::assertSame("Exceeded maximum allowed password length during authentication: user={$username}", Minz_Log::getLastLog());
  210. }
  211. public function testAuthWithValidUsernameAndCorrectCredentialsAndHugeUtf8Password(): void {
  212. $username = 'admin';
  213. $password = str_repeat('a', 10000) . 'é';
  214. $hash = FreshRSS_password_Util::hash($password);
  215. $ok = FreshRSS_FormAuth::checkCredentials(
  216. $username, $hash, $password
  217. );
  218. self::assertFalse($ok);
  219. self::assertSame("Exceeded maximum allowed password length during authentication: user={$username}", Minz_Log::getLastLog());
  220. }
  221. public function testAuthWithValidUsernameAndCorrectCredentialsAndEmptyPasswordFail(): void {
  222. $username = 'admin';
  223. $password = '';
  224. $hash = FreshRSS_password_Util::hash($password);
  225. $ok = FreshRSS_FormAuth::checkCredentials(
  226. $username, $hash, $password
  227. );
  228. self::assertFalse($ok);
  229. self::assertSame("Refusing authentication with empty zero-length password: user={$username}", Minz_Log::getLastLog());
  230. }
  231. public function testAuthWithValidUsernameAndCorrectCredentialsAndTooLongPasswordFail(): void {
  232. $username = 'admin';
  233. $password = str_repeat('a', 73);
  234. $hash = FreshRSS_password_Util::hash($password);
  235. $ok = FreshRSS_FormAuth::checkCredentials(
  236. $username, $hash, $password
  237. );
  238. self::assertFalse($ok);
  239. self::assertSame("Exceeded maximum allowed password length during authentication: user={$username}", Minz_Log::getLastLog());
  240. // It's fine if the user truncates their own password though
  241. // Note: There are separate tests for UTF-8 passwords to test the case
  242. // of a cut-off UTF-8 character at the end, which can't easily be truncated
  243. // by a user.
  244. $password = str_repeat('a', 72);
  245. $ok = FreshRSS_FormAuth::checkCredentials(
  246. $username, $hash, $password
  247. );
  248. self::assertTrue($ok);
  249. self::assertSame('', Minz_Log::getLastLog());
  250. // If truncated down to less than 72 characters, login should fail
  251. $password = str_repeat('a', 71);
  252. $ok = FreshRSS_FormAuth::checkCredentials(
  253. $username, $hash, $password
  254. );
  255. self::assertFalse($ok);
  256. self::assertSame('', Minz_Log::getLastLog());
  257. }
  258. public function testAuthWithValidUsernameAndCorrectCredentialsAndWrongPassAlgorithmFail(): void {
  259. $username = 'admin';
  260. $password = '1234567';
  261. $hash = crypt($password, 'ab'); // Uses `password_verify()` compatible algorithm
  262. $ok = FreshRSS_FormAuth::checkCredentials(
  263. $username, $hash, $password
  264. );
  265. self::assertFalse($ok);
  266. self::assertSame("Invalid hash format: user={$username}", Minz_Log::getLastLog());
  267. }
  268. public function testAuthWithValidUsernameAndAnyCredentialsAndMalformedBcryptHashFail(): void {
  269. $username = 'admin';
  270. $password = 'whatever';
  271. $hash = '$2y$04$00000000$'; // See: https://github.com/php/php-src/security/advisories/GHSA-7fj2-8x79-rjf4
  272. $ok = FreshRSS_FormAuth::checkCredentials(
  273. $username, $hash, $password
  274. );
  275. self::assertFalse($ok);
  276. self::assertSame("Invalid hash format: user={$username}", Minz_Log::getLastLog());
  277. }
  278. public function testAuthWithValidUsernameAndAnyCredentialsAndEmptyHashFail(): void {
  279. $username = 'admin';
  280. $password = 'whatever';
  281. $hash = '';
  282. $ok = FreshRSS_FormAuth::checkCredentials(
  283. $username, $hash, $password
  284. );
  285. self::assertFalse($ok);
  286. self::assertSame("Invalid credential parameters: user={$username}", Minz_Log::getLastLog()); // `ctype_graph($hash)` returns false on empty string values
  287. }
  288. public function testAuthWithValidUsernameAndCorrectHashAsPasswordFail(): void {
  289. $username = 'admin';
  290. $password = 'whatever';
  291. $hash = FreshRSS_password_Util::hash($password);
  292. $ok = FreshRSS_FormAuth::checkCredentials(
  293. $username, $hash, $hash
  294. );
  295. self::assertFalse($ok);
  296. self::assertSame('', Minz_Log::getLastLog());
  297. }
  298. }