TwoFactorAuthTest.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. <?php
  2. namespace Tests;
  3. use PHPUnit\Framework\TestCase;
  4. use RobThree\Auth\TwoFactorAuthException;
  5. use RobThree\Auth\TwoFactorAuth;
  6. class TwoFactorAuthTest extends TestCase
  7. {
  8. use MightNotMakeAssertions;
  9. /**
  10. * @return void
  11. */
  12. public function testConstructorThrowsOnInvalidDigits()
  13. {
  14. $this->expectException(TwoFactorAuthException::class);
  15. new TwoFactorAuth('Test', 0);
  16. }
  17. /**
  18. * @return void
  19. */
  20. public function testConstructorThrowsOnInvalidPeriod()
  21. {
  22. $this->expectException(TwoFactorAuthException::class);
  23. new TwoFactorAuth('Test', 6, 0);
  24. }
  25. /**
  26. * @return void
  27. */
  28. public function testConstructorThrowsOnInvalidAlgorithm()
  29. {
  30. $this->expectException(TwoFactorAuthException::class);
  31. new TwoFactorAuth('Test', 6, 30, 'xxx');
  32. }
  33. /**
  34. * @return void
  35. */
  36. public function testGetCodeReturnsCorrectResults()
  37. {
  38. $tfa = new TwoFactorAuth('Test');
  39. $this->assertEquals('543160', $tfa->getCode('VMR466AB62ZBOKHE', 1426847216));
  40. $this->assertEquals('538532', $tfa->getCode('VMR466AB62ZBOKHE', 0));
  41. }
  42. /**
  43. * @return void
  44. */
  45. public function testEnsureAllTimeProvidersReturnCorrectTime()
  46. {
  47. $tfa = new TwoFactorAuth('Test', 6, 30, 'sha1');
  48. $tfa->ensureCorrectTime(array(
  49. new \RobThree\Auth\Providers\Time\NTPTimeProvider(), // Uses pool.ntp.org by default
  50. //new \RobThree\Auth\Providers\Time\NTPTimeProvider('time.google.com'), // Somehow time.google.com and time.windows.com make travis timeout??
  51. new \RobThree\Auth\Providers\Time\HttpTimeProvider(), // Uses google.com by default
  52. new \RobThree\Auth\Providers\Time\HttpTimeProvider('https://github.com'),
  53. new \RobThree\Auth\Providers\Time\HttpTimeProvider('https://yahoo.com'),
  54. ));
  55. $this->noAssertionsMade();
  56. }
  57. /**
  58. * @return void
  59. */
  60. public function testVerifyCodeWorksCorrectly()
  61. {
  62. $tfa = new TwoFactorAuth('Test', 6, 30);
  63. $this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847190));
  64. $this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 0, 1426847190 + 29)); //Test discrepancy
  65. $this->assertFalse($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 0, 1426847190 + 30)); //Test discrepancy
  66. $this->assertFalse($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 0, 1426847190 - 1)); //Test discrepancy
  67. $this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 + 0)); //Test discrepancy
  68. $this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 + 35)); //Test discrepancy
  69. $this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 - 35)); //Test discrepancy
  70. $this->assertFalse($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 + 65)); //Test discrepancy
  71. $this->assertFalse($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 - 65)); //Test discrepancy
  72. $this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 2, 1426847205 + 65)); //Test discrepancy
  73. $this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 2, 1426847205 - 65)); //Test discrepancy
  74. }
  75. /**
  76. * @return void
  77. */
  78. public function testVerifyCorrectTimeSliceIsReturned()
  79. {
  80. $tfa = new TwoFactorAuth('Test', 6, 30);
  81. // We test with discrepancy 3 (so total of 7 codes: c-3, c-2, c-1, c, c+1, c+2, c+3
  82. // Ensure each corresponding timeslice is returned correctly
  83. $this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '534113', 3, 1426847190, $timeslice1));
  84. $this->assertEquals(47561570, $timeslice1);
  85. $this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '819652', 3, 1426847190, $timeslice2));
  86. $this->assertEquals(47561571, $timeslice2);
  87. $this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '915954', 3, 1426847190, $timeslice3));
  88. $this->assertEquals(47561572, $timeslice3);
  89. $this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 3, 1426847190, $timeslice4));
  90. $this->assertEquals(47561573, $timeslice4);
  91. $this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '348401', 3, 1426847190, $timeslice5));
  92. $this->assertEquals(47561574, $timeslice5);
  93. $this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '648525', 3, 1426847190, $timeslice6));
  94. $this->assertEquals(47561575, $timeslice6);
  95. $this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '170645', 3, 1426847190, $timeslice7));
  96. $this->assertEquals(47561576, $timeslice7);
  97. // Incorrect code should return false and a 0 timeslice
  98. $this->assertFalse($tfa->verifyCode('VMR466AB62ZBOKHE', '111111', 3, 1426847190, $timeslice8));
  99. $this->assertEquals(0, $timeslice8);
  100. }
  101. /**
  102. * @return void
  103. */
  104. public function testGetCodeThrowsOnInvalidBase32String1()
  105. {
  106. $tfa = new TwoFactorAuth('Test');
  107. $this->expectException(TwoFactorAuthException::class);
  108. $tfa->getCode('FOO1BAR8BAZ9'); //1, 8 & 9 are invalid chars
  109. }
  110. /**
  111. * @return void
  112. */
  113. public function testGetCodeThrowsOnInvalidBase32String2()
  114. {
  115. $tfa = new TwoFactorAuth('Test');
  116. $this->expectException(TwoFactorAuthException::class);
  117. $tfa->getCode('mzxw6==='); //Lowercase
  118. }
  119. /**
  120. * @return void
  121. */
  122. public function testKnownBase32DecodeTestVectors()
  123. {
  124. // We usually don't test internals (e.g. privates) but since we rely heavily on base32 decoding and don't want
  125. // to expose this method nor do we want to give people the possibility of implementing / providing their own base32
  126. // decoding/decoder (as we do with Rng/QR providers for example) we simply test the private base32Decode() method
  127. // with some known testvectors **only** to ensure base32 decoding works correctly following RFC's so there won't
  128. // be any bugs hiding in there. We **could** 'fool' ourselves by calling the public getCode() method (which uses
  129. // base32decode internally) and then make sure getCode's output (in digits) equals expected output since that would
  130. // mean the base32Decode() works as expected but that **could** hide some subtle bug(s) in decoding the base32 string.
  131. // "In general, you don't want to break any encapsulation for the sake of testing (or as Mom used to say, "don't
  132. // expose your privates!"). Most of the time, you should be able to test a class by exercising its public methods."
  133. // Dave Thomas and Andy Hunt -- "Pragmatic Unit Testing
  134. $tfa = new TwoFactorAuth('Test');
  135. $method = new \ReflectionMethod(TwoFactorAuth::class, 'base32Decode');
  136. $method->setAccessible(true);
  137. // Test vectors from: https://tools.ietf.org/html/rfc4648#page-12
  138. $this->assertEquals('', $method->invoke($tfa, ''));
  139. $this->assertEquals('f', $method->invoke($tfa, 'MY======'));
  140. $this->assertEquals('fo', $method->invoke($tfa, 'MZXQ===='));
  141. $this->assertEquals('foo', $method->invoke($tfa, 'MZXW6==='));
  142. $this->assertEquals('foob', $method->invoke($tfa, 'MZXW6YQ='));
  143. $this->assertEquals('fooba', $method->invoke($tfa, 'MZXW6YTB'));
  144. $this->assertEquals('foobar', $method->invoke($tfa, 'MZXW6YTBOI======'));
  145. }
  146. /**
  147. * @return void
  148. */
  149. public function testKnownBase32DecodeUnpaddedTestVectors()
  150. {
  151. // See testKnownBase32DecodeTestVectors() for the rationale behind testing the private base32Decode() method.
  152. // This test ensures that strings without the padding-char ('=') are also decoded correctly.
  153. // https://tools.ietf.org/html/rfc4648#page-4:
  154. // "In some circumstances, the use of padding ("=") in base-encoded data is not required or used."
  155. $tfa = new TwoFactorAuth('Test');
  156. $method = new \ReflectionMethod(TwoFactorAuth::class, 'base32Decode');
  157. $method->setAccessible(true);
  158. // Test vectors from: https://tools.ietf.org/html/rfc4648#page-12
  159. $this->assertEquals('', $method->invoke($tfa, ''));
  160. $this->assertEquals('f', $method->invoke($tfa, 'MY'));
  161. $this->assertEquals('fo', $method->invoke($tfa, 'MZXQ'));
  162. $this->assertEquals('foo', $method->invoke($tfa, 'MZXW6'));
  163. $this->assertEquals('foob', $method->invoke($tfa, 'MZXW6YQ'));
  164. $this->assertEquals('fooba', $method->invoke($tfa, 'MZXW6YTB'));
  165. $this->assertEquals('foobar', $method->invoke($tfa, 'MZXW6YTBOI'));
  166. }
  167. /**
  168. * @return void
  169. */
  170. public function testKnownTestVectors_sha1()
  171. {
  172. //Known test vectors for SHA1: https://tools.ietf.org/html/rfc6238#page-15
  173. $secret = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ'; //== base32encode('12345678901234567890')
  174. $tfa = new TwoFactorAuth('Test', 8, 30, 'sha1');
  175. $this->assertEquals('94287082', $tfa->getCode($secret, 59));
  176. $this->assertEquals('07081804', $tfa->getCode($secret, 1111111109));
  177. $this->assertEquals('14050471', $tfa->getCode($secret, 1111111111));
  178. $this->assertEquals('89005924', $tfa->getCode($secret, 1234567890));
  179. $this->assertEquals('69279037', $tfa->getCode($secret, 2000000000));
  180. $this->assertEquals('65353130', $tfa->getCode($secret, 20000000000));
  181. }
  182. /**
  183. * @return void
  184. */
  185. public function testKnownTestVectors_sha256()
  186. {
  187. //Known test vectors for SHA256: https://tools.ietf.org/html/rfc6238#page-15
  188. $secret = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA'; //== base32encode('12345678901234567890123456789012')
  189. $tfa = new TwoFactorAuth('Test', 8, 30, 'sha256');
  190. $this->assertEquals('46119246', $tfa->getCode($secret, 59));
  191. $this->assertEquals('68084774', $tfa->getCode($secret, 1111111109));
  192. $this->assertEquals('67062674', $tfa->getCode($secret, 1111111111));
  193. $this->assertEquals('91819424', $tfa->getCode($secret, 1234567890));
  194. $this->assertEquals('90698825', $tfa->getCode($secret, 2000000000));
  195. $this->assertEquals('77737706', $tfa->getCode($secret, 20000000000));
  196. }
  197. /**
  198. * @return void
  199. */
  200. public function testKnownTestVectors_sha512()
  201. {
  202. //Known test vectors for SHA512: https://tools.ietf.org/html/rfc6238#page-15
  203. $secret = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA'; //== base32encode('1234567890123456789012345678901234567890123456789012345678901234')
  204. $tfa = new TwoFactorAuth('Test', 8, 30, 'sha512');
  205. $this->assertEquals('90693936', $tfa->getCode($secret, 59));
  206. $this->assertEquals('25091201', $tfa->getCode($secret, 1111111109));
  207. $this->assertEquals('99943326', $tfa->getCode($secret, 1111111111));
  208. $this->assertEquals('93441116', $tfa->getCode($secret, 1234567890));
  209. $this->assertEquals('38618901', $tfa->getCode($secret, 2000000000));
  210. $this->assertEquals('47863826', $tfa->getCode($secret, 20000000000));
  211. }
  212. }