blob: ca00df9d172cc30a91f4e335a4367d84aa20d0ca [file] [log] [blame]
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001<?php
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01002
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02003namespace Tests;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01004
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02005use PHPUnit\Framework\TestCase;
6use RobThree\Auth\TwoFactorAuthException;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01007use RobThree\Auth\TwoFactorAuth;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01008
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02009class TwoFactorAuthTest extends TestCase
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010010{
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020011 use MightNotMakeAssertions;
12
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010013 /**
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020014 * @return void
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010015 */
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020016 public function testConstructorThrowsOnInvalidDigits()
17 {
18 $this->expectException(TwoFactorAuthException::class);
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010019
20 new TwoFactorAuth('Test', 0);
21 }
22
23 /**
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020024 * @return void
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010025 */
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020026 public function testConstructorThrowsOnInvalidPeriod()
27 {
28 $this->expectException(TwoFactorAuthException::class);
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010029
30 new TwoFactorAuth('Test', 6, 0);
31 }
32
33 /**
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020034 * @return void
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010035 */
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020036 public function testConstructorThrowsOnInvalidAlgorithm()
37 {
38 $this->expectException(TwoFactorAuthException::class);
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010039
40 new TwoFactorAuth('Test', 6, 30, 'xxx');
41 }
42
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020043 /**
44 * @return void
45 */
46 public function testGetCodeReturnsCorrectResults()
47 {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010048 $tfa = new TwoFactorAuth('Test');
49 $this->assertEquals('543160', $tfa->getCode('VMR466AB62ZBOKHE', 1426847216));
50 $this->assertEquals('538532', $tfa->getCode('VMR466AB62ZBOKHE', 0));
51 }
52
53 /**
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020054 * @return void
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010055 */
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020056 public function testEnsureAllTimeProvidersReturnCorrectTime()
57 {
58 $tfa = new TwoFactorAuth('Test', 6, 30, 'sha1');
59 $tfa->ensureCorrectTime(array(
60 new \RobThree\Auth\Providers\Time\NTPTimeProvider(), // Uses pool.ntp.org by default
61 //new \RobThree\Auth\Providers\Time\NTPTimeProvider('time.google.com'), // Somehow time.google.com and time.windows.com make travis timeout??
62 new \RobThree\Auth\Providers\Time\HttpTimeProvider(), // Uses google.com by default
63 new \RobThree\Auth\Providers\Time\HttpTimeProvider('https://github.com'),
64 new \RobThree\Auth\Providers\Time\HttpTimeProvider('https://yahoo.com'),
65 ));
66 $this->noAssertionsMade();
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010067 }
68
69 /**
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020070 * @return void
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010071 */
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020072 public function testVerifyCodeWorksCorrectly()
73 {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010074 $tfa = new TwoFactorAuth('Test', 6, 30);
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020075 $this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847190));
76 $this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 0, 1426847190 + 29)); //Test discrepancy
77 $this->assertFalse($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 0, 1426847190 + 30)); //Test discrepancy
78 $this->assertFalse($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 0, 1426847190 - 1)); //Test discrepancy
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010079
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020080 $this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 + 0)); //Test discrepancy
81 $this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 + 35)); //Test discrepancy
82 $this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 - 35)); //Test discrepancy
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010083
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020084 $this->assertFalse($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 + 65)); //Test discrepancy
85 $this->assertFalse($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 - 65)); //Test discrepancy
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010086
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020087 $this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 2, 1426847205 + 65)); //Test discrepancy
88 $this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 2, 1426847205 - 65)); //Test discrepancy
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010089 }
90
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020091 /**
92 * @return void
93 */
94 public function testVerifyCorrectTimeSliceIsReturned()
95 {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010096 $tfa = new TwoFactorAuth('Test', 6, 30);
97
98 // We test with discrepancy 3 (so total of 7 codes: c-3, c-2, c-1, c, c+1, c+2, c+3
99 // Ensure each corresponding timeslice is returned correctly
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200100 $this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '534113', 3, 1426847190, $timeslice1));
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100101 $this->assertEquals(47561570, $timeslice1);
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200102 $this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '819652', 3, 1426847190, $timeslice2));
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100103 $this->assertEquals(47561571, $timeslice2);
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200104 $this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '915954', 3, 1426847190, $timeslice3));
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100105 $this->assertEquals(47561572, $timeslice3);
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200106 $this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 3, 1426847190, $timeslice4));
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100107 $this->assertEquals(47561573, $timeslice4);
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200108 $this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '348401', 3, 1426847190, $timeslice5));
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100109 $this->assertEquals(47561574, $timeslice5);
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200110 $this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '648525', 3, 1426847190, $timeslice6));
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100111 $this->assertEquals(47561575, $timeslice6);
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200112 $this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '170645', 3, 1426847190, $timeslice7));
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100113 $this->assertEquals(47561576, $timeslice7);
114
115 // Incorrect code should return false and a 0 timeslice
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200116 $this->assertFalse($tfa->verifyCode('VMR466AB62ZBOKHE', '111111', 3, 1426847190, $timeslice8));
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100117 $this->assertEquals(0, $timeslice8);
118 }
119
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100120 /**
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200121 * @return void
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100122 */
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200123 public function testGetCodeThrowsOnInvalidBase32String1()
124 {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100125 $tfa = new TwoFactorAuth('Test');
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200126
127 $this->expectException(TwoFactorAuthException::class);
128
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100129 $tfa->getCode('FOO1BAR8BAZ9'); //1, 8 & 9 are invalid chars
130 }
131
132 /**
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200133 * @return void
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100134 */
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200135 public function testGetCodeThrowsOnInvalidBase32String2()
136 {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100137 $tfa = new TwoFactorAuth('Test');
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200138
139 $this->expectException(TwoFactorAuthException::class);
140
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100141 $tfa->getCode('mzxw6==='); //Lowercase
142 }
143
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200144 /**
145 * @return void
146 */
147 public function testKnownBase32DecodeTestVectors()
148 {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100149 // We usually don't test internals (e.g. privates) but since we rely heavily on base32 decoding and don't want
150 // to expose this method nor do we want to give people the possibility of implementing / providing their own base32
151 // decoding/decoder (as we do with Rng/QR providers for example) we simply test the private base32Decode() method
152 // with some known testvectors **only** to ensure base32 decoding works correctly following RFC's so there won't
153 // be any bugs hiding in there. We **could** 'fool' ourselves by calling the public getCode() method (which uses
154 // base32decode internally) and then make sure getCode's output (in digits) equals expected output since that would
155 // mean the base32Decode() works as expected but that **could** hide some subtle bug(s) in decoding the base32 string.
156
157 // "In general, you don't want to break any encapsulation for the sake of testing (or as Mom used to say, "don't
158 // expose your privates!"). Most of the time, you should be able to test a class by exercising its public methods."
159 // Dave Thomas and Andy Hunt -- "Pragmatic Unit Testing
160 $tfa = new TwoFactorAuth('Test');
161
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200162 $method = new \ReflectionMethod(TwoFactorAuth::class, 'base32Decode');
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100163 $method->setAccessible(true);
164
165 // Test vectors from: https://tools.ietf.org/html/rfc4648#page-12
166 $this->assertEquals('', $method->invoke($tfa, ''));
167 $this->assertEquals('f', $method->invoke($tfa, 'MY======'));
168 $this->assertEquals('fo', $method->invoke($tfa, 'MZXQ===='));
169 $this->assertEquals('foo', $method->invoke($tfa, 'MZXW6==='));
170 $this->assertEquals('foob', $method->invoke($tfa, 'MZXW6YQ='));
171 $this->assertEquals('fooba', $method->invoke($tfa, 'MZXW6YTB'));
172 $this->assertEquals('foobar', $method->invoke($tfa, 'MZXW6YTBOI======'));
173 }
174
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200175 /**
176 * @return void
177 */
178 public function testKnownBase32DecodeUnpaddedTestVectors()
179 {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100180 // See testKnownBase32DecodeTestVectors() for the rationale behind testing the private base32Decode() method.
181 // This test ensures that strings without the padding-char ('=') are also decoded correctly.
182 // https://tools.ietf.org/html/rfc4648#page-4:
183 // "In some circumstances, the use of padding ("=") in base-encoded data is not required or used."
184 $tfa = new TwoFactorAuth('Test');
185
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200186 $method = new \ReflectionMethod(TwoFactorAuth::class, 'base32Decode');
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100187 $method->setAccessible(true);
188
189 // Test vectors from: https://tools.ietf.org/html/rfc4648#page-12
190 $this->assertEquals('', $method->invoke($tfa, ''));
191 $this->assertEquals('f', $method->invoke($tfa, 'MY'));
192 $this->assertEquals('fo', $method->invoke($tfa, 'MZXQ'));
193 $this->assertEquals('foo', $method->invoke($tfa, 'MZXW6'));
194 $this->assertEquals('foob', $method->invoke($tfa, 'MZXW6YQ'));
195 $this->assertEquals('fooba', $method->invoke($tfa, 'MZXW6YTB'));
196 $this->assertEquals('foobar', $method->invoke($tfa, 'MZXW6YTBOI'));
197 }
198
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200199 /**
200 * @return void
201 */
202 public function testKnownTestVectors_sha1()
203 {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100204 //Known test vectors for SHA1: https://tools.ietf.org/html/rfc6238#page-15
205 $secret = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ'; //== base32encode('12345678901234567890')
206 $tfa = new TwoFactorAuth('Test', 8, 30, 'sha1');
207 $this->assertEquals('94287082', $tfa->getCode($secret, 59));
208 $this->assertEquals('07081804', $tfa->getCode($secret, 1111111109));
209 $this->assertEquals('14050471', $tfa->getCode($secret, 1111111111));
210 $this->assertEquals('89005924', $tfa->getCode($secret, 1234567890));
211 $this->assertEquals('69279037', $tfa->getCode($secret, 2000000000));
212 $this->assertEquals('65353130', $tfa->getCode($secret, 20000000000));
213 }
214
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200215 /**
216 * @return void
217 */
218 public function testKnownTestVectors_sha256()
219 {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100220 //Known test vectors for SHA256: https://tools.ietf.org/html/rfc6238#page-15
221 $secret = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA'; //== base32encode('12345678901234567890123456789012')
222 $tfa = new TwoFactorAuth('Test', 8, 30, 'sha256');
223 $this->assertEquals('46119246', $tfa->getCode($secret, 59));
224 $this->assertEquals('68084774', $tfa->getCode($secret, 1111111109));
225 $this->assertEquals('67062674', $tfa->getCode($secret, 1111111111));
226 $this->assertEquals('91819424', $tfa->getCode($secret, 1234567890));
227 $this->assertEquals('90698825', $tfa->getCode($secret, 2000000000));
228 $this->assertEquals('77737706', $tfa->getCode($secret, 20000000000));
229 }
230
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200231 /**
232 * @return void
233 */
234 public function testKnownTestVectors_sha512()
235 {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100236 //Known test vectors for SHA512: https://tools.ietf.org/html/rfc6238#page-15
237 $secret = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA'; //== base32encode('1234567890123456789012345678901234567890123456789012345678901234')
238 $tfa = new TwoFactorAuth('Test', 8, 30, 'sha512');
239 $this->assertEquals('90693936', $tfa->getCode($secret, 59));
240 $this->assertEquals('25091201', $tfa->getCode($secret, 1111111109));
241 $this->assertEquals('99943326', $tfa->getCode($secret, 1111111111));
242 $this->assertEquals('93441116', $tfa->getCode($secret, 1234567890));
243 $this->assertEquals('38618901', $tfa->getCode($secret, 2000000000));
244 $this->assertEquals('47863826', $tfa->getCode($secret, 20000000000));
245 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100246}