diff --git a/docs/getting-started.md b/docs/getting-started.md index e25df8b..ae1612d 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -7,28 +7,33 @@ title: Getting Started The best way of making use of this project is by installing it with [composer](https://getcomposer.org/doc/01-basic-usage.md). -``` -php composer.phar require robthree/twofactorauth -``` - -or if you have composer installed globally - ``` composer require robthree/twofactorauth ``` ## 2. Create an instance -Now you can create an instance for use with your code +`TwoFactorAuth` constructor requires an object able to provide a QR Code image. It is the only mandatory argument. This lets you select your preferred QR Code generator/library. + +See [QR code providers documentation](qr-codes.md) for more information about the different possibilites. + +Example code: ```php use RobThree\Auth\TwoFactorAuth; - -$tfa = new TwoFactorAuth(); +use RobThree\Auth\Providers\Qr\BaconQrCodeProvider; // if using Bacon +use RobThree\Auth\Providers\Qr\EndroidQrCodeProvider; // if using Endroid + +// using Bacon +$tfa = new TwoFactorAuth(new BaconQrCodeProvider()); +// using Endroid +$tfa = new TwoFactorAuth(new EndroidQrCodeProvider()); +// using a custom object implementing IQRCodeProvider interface +$tfa = new TwoFactorAuth(new MyQrCodeProvider()); +// using named argument and a variable +$tfa = new TwoFactorAuth(qrcodeprovider: $qrGenerator); ``` -**Note:** if you are not using a framework that uses composer, you should [include the composer loader yourself](https://getcomposer.org/doc/01-basic-usage.md#autoloading) - ## 3. Shared secrets When your user is setting up two-factor, or multi-factor, authentication in your project, you can create a secret from the instance. diff --git a/docs/qr-codes.md b/docs/qr-codes.md index 7236198..7689856 100644 --- a/docs/qr-codes.md +++ b/docs/qr-codes.md @@ -5,7 +5,7 @@ title: QR Codes An alternative way of communicating the secret to the user is through the use of [QR Codes](http://en.wikipedia.org/wiki/QR_code) which most if not all authenticator mobile apps can scan. -This can avoid accidental typing errors and also pre-set some text values within the users app. +This can avoid accidental typing errors and also pre-set some text values within the two factor authentication mobile application. You can display the QR Code as a base64 encoded image using the instance as follows, supplying the users name or other public identifier as the first argument @@ -16,18 +16,6 @@ You can display the QR Code as a base64 encoded image using the instance as foll You can also specify a size as a third argument which is 200 by default. -**Note:** by default, the QR code returned by the instance is generated from a third party across the internet. If the third party is encountering problems or is not available from where you have hosted your code, your user will likely experience a delay in seeing the QR code, if it even loads at all. This can be overcome with offline providers configured when you create the instance. - -## Online Providers - -[QRServerProvider](qr-codes/qr-server.md) (default) - -**Warning:** Whilst it is the default, this provider is not suggested for applications where absolute security is needed, because it uses an external service for the QR code generation. You can make use of the included offline providers listed below which generate locally. - -[ImageChartsQRCodeProvider](qr-codes/image-charts.md) - -[QRicketProvider](qr-codes/qrickit.md) - ## Offline Providers [EndroidQrCodeProvider](qr-codes/endroid.md) and EndroidQrCodeWithLogoProvider @@ -38,23 +26,33 @@ You can also specify a size as a third argument which is 200 by default. ## Custom Provider -If you wish to make your own QR Code provider to reference another service or library, it must implement the [IQRCodeProvider interface](https://github.com/RobThree/TwoFactorAuth/blob/master/lib/Providers/Qr/IQRCodeProvider.php). +If you wish to make your own QR Code provider to reference another service or library, it must implement the [IQRCodeProvider interface](../lib/Providers/Qr/IQRCodeProvider.php). It is recommended to use similar constructor arguments as the included providers to avoid big shifts when trying different providers. -## Using a specific provider - -If you do not want to use the default QR code provider, you can specify the one you want to use when you create your instance. +Example: ```php use RobThree\Auth\TwoFactorAuth; +// using a custom object implementing IQRCodeProvider +$tfa = new TwoFactorAuth(new MyQrCodeProvider()); +// using named argument and a variable +$tfa = new TwoFactorAuth(qrcodeprovider: $qrGenerator); +``` + +## Online Providers -$qrCodeProvider = new YourChosenProvider(); +**Warning:** Using an external service for generating QR codes encoding authentication secrets is **not** recommended! You should instead make use of the included offline providers listed above. -$tfa = new TwoFactorAuth( - issuer: "Your Company Or App Name", - qrcodeprovider: $qrCodeProvider -); -``` +* Gogr.me: [QRServerProvider](qr-codes/qr-server.md) +* Image Charts: [ImageChartsQRCodeProvider](qr-codes/image-charts.md) +* Qrickit: [QRicketProvider](qr-codes/qrickit.md) +* Google Charts: [GoogleChartsQrCodeProvider](qr-codes/google-charts.md) -As you create a new instance of your provider, you can supply any extra configuration there. +Example: + +```php +use RobThree\Auth\TwoFactorAuth; +use RobThree\Auth\Providers\Qr\GoogleChartsQrCodeProvider; +$tfa = new TwoFactorAuth(new GoogleChartsQrCodeProvider()); +``` diff --git a/docs/qr-codes/google-charts.md b/docs/qr-codes/google-charts.md new file mode 100644 index 0000000..9b70f37 --- /dev/null +++ b/docs/qr-codes/google-charts.md @@ -0,0 +1,15 @@ +--- +layout: post +title: QR GoogleCharts +--- + +See: https://developers.google.com/chart/infographics/docs/qr_codes + +## Optional Configuration + +Argument | Default value +------------------------|--------------- +`$verifyssl` | `false` +`$errorcorrectionlevel` | `'L'` +`$margin` | `4` +`$encoding` | `'UTF-8'` diff --git a/lib/Providers/Qr/QRicketProvider.php b/lib/Providers/Qr/QRicketProvider.php index d976b5f..d9c4945 100644 --- a/lib/Providers/Qr/QRicketProvider.php +++ b/lib/Providers/Qr/QRicketProvider.php @@ -43,6 +43,6 @@ public function getUrl(string $qrText, int $size): string 'd' => $qrText, ); - return 'http://qrickit.com/api/qr?' . http_build_query($queryParameters); + return 'https://qrickit.com/api/qr?' . http_build_query($queryParameters); } } diff --git a/lib/TwoFactorAuth.php b/lib/TwoFactorAuth.php index 82b8501..cd6932c 100644 --- a/lib/TwoFactorAuth.php +++ b/lib/TwoFactorAuth.php @@ -7,7 +7,6 @@ use function hash_equals; use RobThree\Auth\Providers\Qr\IQRCodeProvider; -use RobThree\Auth\Providers\Qr\QRServerProvider; use RobThree\Auth\Providers\Rng\CSRNGProvider; use RobThree\Auth\Providers\Rng\IRNGProvider; use RobThree\Auth\Providers\Time\HttpTimeProvider; @@ -29,11 +28,11 @@ class TwoFactorAuth private static array $_base32lookup = array(); public function __construct( + private IQRCodeProvider $qrcodeprovider, private readonly ?string $issuer = null, private readonly int $digits = 6, private readonly int $period = 30, private readonly Algorithm $algorithm = Algorithm::Sha1, - private ?IQRCodeProvider $qrcodeprovider = null, private ?IRNGProvider $rngprovider = null, private ?ITimeProvider $timeprovider = null ) { @@ -111,11 +110,10 @@ public function getQRCodeImageAsDataUri(string $label, #[SensitiveParameter] str throw new TwoFactorAuthException('Size must be > 0'); } - $qrcodeprovider = $this->getQrCodeProvider(); return 'data:' - . $qrcodeprovider->getMimeType() + . $this->qrcodeprovider->getMimeType() . ';base64,' - . base64_encode($qrcodeprovider->getQRCodeImage($this->getQRText($label, $secret), $size)); + . base64_encode($this->qrcodeprovider->getQRCodeImage($this->getQRText($label, $secret), $size)); } /** @@ -161,12 +159,6 @@ public function getQRText(string $label, #[SensitiveParameter] string $secret): . '&digits=' . $this->digits; } - public function getQrCodeProvider(): IQRCodeProvider - { - // Set default QR Code provider if none was specified - return $this->qrcodeprovider ??= new QRServerProvider(); - } - /** * @throws TwoFactorAuthException */ diff --git a/tests/Providers/Qr/IQRCodeProviderTest.php b/tests/Providers/Qr/IQRCodeProviderTest.php index a9f7e82..1d1ba24 100644 --- a/tests/Providers/Qr/IQRCodeProviderTest.php +++ b/tests/Providers/Qr/IQRCodeProviderTest.php @@ -7,6 +7,7 @@ use PHPUnit\Framework\TestCase; use RobThree\Auth\Algorithm; use RobThree\Auth\Providers\Qr\HandlesDataUri; +use RobThree\Auth\Providers\Qr\IQRCodeProvider; use RobThree\Auth\TwoFactorAuth; use RobThree\Auth\TwoFactorAuthException; @@ -14,11 +15,16 @@ class IQRCodeProviderTest extends TestCase { use HandlesDataUri; - public function testTotpUriIsCorrect(): void + protected IQRCodeProvider $qr; + + protected function setUp(): void { - $qr = new TestQrProvider(); + $this->qr = new TestQrProvider(); + } - $tfa = new TwoFactorAuth('Test&Issuer', 6, 30, Algorithm::Sha1, $qr); + public function testTotpUriIsCorrect(): void + { + $tfa = new TwoFactorAuth($this->qr, 'Test&Issuer', 6, 30, Algorithm::Sha1); $data = $this->DecodeDataUri($tfa->getQRCodeImageAsDataUri('Test&Label', 'VMR466AB62ZBOKHE')); $this->assertSame('test/test', $data['mimetype']); $this->assertSame('base64', $data['encoding']); @@ -27,14 +33,12 @@ public function testTotpUriIsCorrect(): void public function testTotpUriIsCorrectNoIssuer(): void { - $qr = new TestQrProvider(); - /** * The library specifies the issuer is null by default however in PHP 8.1 * there is a deprecation warning for passing null as a string argument to rawurlencode */ - $tfa = new TwoFactorAuth(null, 6, 30, Algorithm::Sha1, $qr); + $tfa = new TwoFactorAuth($this->qr, null, 6, 30, Algorithm::Sha1); $data = $this->DecodeDataUri($tfa->getQRCodeImageAsDataUri('Test&Label', 'VMR466AB62ZBOKHE')); $this->assertSame('test/test', $data['mimetype']); $this->assertSame('base64', $data['encoding']); @@ -43,9 +47,7 @@ public function testTotpUriIsCorrectNoIssuer(): void public function testGetQRCodeImageAsDataUriThrowsOnInvalidSize(): void { - $qr = new TestQrProvider(); - - $tfa = new TwoFactorAuth('Test', 6, 30, Algorithm::Sha1, $qr); + $tfa = new TwoFactorAuth($this->qr, 'Test', 6, 30, Algorithm::Sha1); $this->expectException(TwoFactorAuthException::class); diff --git a/tests/Providers/Rng/IRNGProviderTest.php b/tests/Providers/Rng/IRNGProviderTest.php index fd2c742..4e5608c 100644 --- a/tests/Providers/Rng/IRNGProviderTest.php +++ b/tests/Providers/Rng/IRNGProviderTest.php @@ -7,12 +7,13 @@ use PHPUnit\Framework\TestCase; use RobThree\Auth\Algorithm; use RobThree\Auth\TwoFactorAuth; +use Tests\Providers\Qr\TestQrProvider; class IRNGProviderTest extends TestCase { public function testCreateSecret(): void { - $tfa = new TwoFactorAuth('Test', 6, 30, Algorithm::Sha1, null, null); + $tfa = new TwoFactorAuth(new TestQrProvider(), 'Test', 6, 30, Algorithm::Sha1, null, null); $this->assertIsString($tfa->createSecret()); } } diff --git a/tests/Providers/Time/ITimeProviderTest.php b/tests/Providers/Time/ITimeProviderTest.php index f67ac85..1f112e6 100644 --- a/tests/Providers/Time/ITimeProviderTest.php +++ b/tests/Providers/Time/ITimeProviderTest.php @@ -8,6 +8,7 @@ use RobThree\Auth\Algorithm; use RobThree\Auth\TwoFactorAuth; use RobThree\Auth\TwoFactorAuthException; +use Tests\Providers\Qr\TestQrProvider; class ITimeProviderTest extends TestCase { @@ -17,7 +18,7 @@ public function testEnsureCorrectTimeDoesNotThrowForCorrectTime(): void $tpr1 = new TestTimeProvider(123); $tpr2 = new TestTimeProvider(128); - $tfa = new TwoFactorAuth('Test', 6, 30, Algorithm::Sha1, null, null, $tpr1); + $tfa = new TwoFactorAuth(new TestQrProvider(), 'Test', 6, 30, Algorithm::Sha1, null, $tpr1); $tfa->ensureCorrectTime(array($tpr2)); // 128 - 123 = 5 => within default leniency } @@ -26,7 +27,7 @@ public function testEnsureCorrectTimeThrowsOnIncorrectTime(): void $tpr1 = new TestTimeProvider(123); $tpr2 = new TestTimeProvider(124); - $tfa = new TwoFactorAuth('Test', 6, 30, Algorithm::Sha1, null, null, $tpr1); + $tfa = new TwoFactorAuth(new TestQrProvider(), 'Test', 6, 30, Algorithm::Sha1, null, $tpr1); $this->expectException(TwoFactorAuthException::class); @@ -36,7 +37,7 @@ public function testEnsureCorrectTimeThrowsOnIncorrectTime(): void public function testEnsureDefaultTimeProviderReturnsCorrectTime(): void { $this->expectNotToPerformAssertions(); - $tfa = new TwoFactorAuth('Test', 6, 30, Algorithm::Sha1); + $tfa = new TwoFactorAuth(new TestQrProvider(), 'Test', 6, 30, Algorithm::Sha1); $tfa->ensureCorrectTime(array(new TestTimeProvider(time())), 1); // Use a leniency of 1, should the time change between both time() calls } } diff --git a/tests/TwoFactorAuthTest.php b/tests/TwoFactorAuthTest.php index 0fa9f11..98d2aba 100644 --- a/tests/TwoFactorAuthTest.php +++ b/tests/TwoFactorAuthTest.php @@ -11,6 +11,7 @@ use RobThree\Auth\Providers\Time\NTPTimeProvider; use RobThree\Auth\TwoFactorAuth; use RobThree\Auth\TwoFactorAuthException; +use Tests\Providers\Qr\TestQrProvider; class TwoFactorAuthTest extends TestCase { @@ -18,26 +19,26 @@ public function testConstructorThrowsOnInvalidDigits(): void { $this->expectException(TwoFactorAuthException::class); - new TwoFactorAuth('Test', 0); + new TwoFactorAuth(new TestQrProvider(), 'Test', 0); } public function testConstructorThrowsOnInvalidPeriod(): void { $this->expectException(TwoFactorAuthException::class); - new TwoFactorAuth('Test', 6, 0); + new TwoFactorAuth(new TestQrProvider(), 'Test', 6, 0); } public function testGetCodeReturnsCorrectResults(): void { - $tfa = new TwoFactorAuth('Test'); + $tfa = new TwoFactorAuth(new TestQrProvider(), 'Test'); $this->assertSame('543160', $tfa->getCode('VMR466AB62ZBOKHE', 1426847216)); $this->assertSame('538532', $tfa->getCode('VMR466AB62ZBOKHE', 0)); } public function testEnsureAllTimeProvidersReturnCorrectTime(): void { - $tfa = new TwoFactorAuth('Test', 6, 30, Algorithm::Sha1); + $tfa = new TwoFactorAuth(new TestQrProvider(), 'Test', 6, 30, Algorithm::Sha1); $tfa->ensureCorrectTime(array( new NTPTimeProvider(), // Uses pool.ntp.org by default //new \RobThree\Auth\Providers\Time\NTPTimeProvider('time.google.com'), // Somehow time.google.com and time.windows.com make travis timeout?? @@ -50,7 +51,7 @@ public function testEnsureAllTimeProvidersReturnCorrectTime(): void public function testVerifyCodeWorksCorrectly(): void { - $tfa = new TwoFactorAuth('Test', 6, 30); + $tfa = new TwoFactorAuth(new TestQrProvider(), 'Test', 6, 30); $this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847190)); $this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 0, 1426847190 + 29)); //Test discrepancy $this->assertFalse($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 0, 1426847190 + 30)); //Test discrepancy @@ -69,7 +70,7 @@ public function testVerifyCodeWorksCorrectly(): void public function testVerifyCorrectTimeSliceIsReturned(): void { - $tfa = new TwoFactorAuth('Test', 6, 30); + $tfa = new TwoFactorAuth(new TestQrProvider(), 'Test', 6, 30); // We test with discrepancy 3 (so total of 7 codes: c-3, c-2, c-1, c, c+1, c+2, c+3 // Ensure each corresponding timeslice is returned correctly @@ -95,7 +96,7 @@ public function testVerifyCorrectTimeSliceIsReturned(): void public function testGetCodeThrowsOnInvalidBase32String1(): void { - $tfa = new TwoFactorAuth('Test'); + $tfa = new TwoFactorAuth(new TestQrProvider(), 'Test'); $this->expectException(TwoFactorAuthException::class); @@ -104,7 +105,7 @@ public function testGetCodeThrowsOnInvalidBase32String1(): void public function testGetCodeThrowsOnInvalidBase32String2(): void { - $tfa = new TwoFactorAuth('Test'); + $tfa = new TwoFactorAuth(new TestQrProvider(), 'Test'); $this->expectException(TwoFactorAuthException::class); @@ -124,7 +125,7 @@ public function testKnownBase32DecodeTestVectors(): void // "In general, you don't want to break any encapsulation for the sake of testing (or as Mom used to say, "don't // expose your privates!"). Most of the time, you should be able to test a class by exercising its public methods." // Dave Thomas and Andy Hunt -- "Pragmatic Unit Testing - $tfa = new TwoFactorAuth('Test'); + $tfa = new TwoFactorAuth(new TestQrProvider(), 'Test'); $method = new ReflectionMethod(TwoFactorAuth::class, 'base32Decode'); @@ -144,7 +145,7 @@ public function testKnownBase32DecodeUnpaddedTestVectors(): void // This test ensures that strings without the padding-char ('=') are also decoded correctly. // https://tools.ietf.org/html/rfc4648#page-4: // "In some circumstances, the use of padding ("=") in base-encoded data is not required or used." - $tfa = new TwoFactorAuth('Test'); + $tfa = new TwoFactorAuth(new TestQrProvider(), 'Test'); $method = new ReflectionMethod(TwoFactorAuth::class, 'base32Decode'); @@ -162,7 +163,7 @@ public function testKnownTestVectors_sha1(): void { //Known test vectors for SHA1: https://tools.ietf.org/html/rfc6238#page-15 $secret = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ'; //== base32encode('12345678901234567890') - $tfa = new TwoFactorAuth('Test', 8, 30, Algorithm::Sha1); + $tfa = new TwoFactorAuth(new TestQrProvider(), 'Test', 8, 30, Algorithm::Sha1); $this->assertSame('94287082', $tfa->getCode($secret, 59)); $this->assertSame('07081804', $tfa->getCode($secret, 1111111109)); $this->assertSame('14050471', $tfa->getCode($secret, 1111111111)); @@ -175,7 +176,7 @@ public function testKnownTestVectors_sha256(): void { //Known test vectors for SHA256: https://tools.ietf.org/html/rfc6238#page-15 $secret = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA'; //== base32encode('12345678901234567890123456789012') - $tfa = new TwoFactorAuth('Test', 8, 30, Algorithm::Sha256); + $tfa = new TwoFactorAuth(new TestQrProvider(), 'Test', 8, 30, Algorithm::Sha256); $this->assertSame('46119246', $tfa->getCode($secret, 59)); $this->assertSame('68084774', $tfa->getCode($secret, 1111111109)); $this->assertSame('67062674', $tfa->getCode($secret, 1111111111)); @@ -188,7 +189,7 @@ public function testKnownTestVectors_sha512(): void { //Known test vectors for SHA512: https://tools.ietf.org/html/rfc6238#page-15 $secret = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA'; //== base32encode('1234567890123456789012345678901234567890123456789012345678901234') - $tfa = new TwoFactorAuth('Test', 8, 30, Algorithm::Sha512); + $tfa = new TwoFactorAuth(new TestQrProvider(), 'Test', 8, 30, Algorithm::Sha512); $this->assertSame('90693936', $tfa->getCode($secret, 59)); $this->assertSame('25091201', $tfa->getCode($secret, 1111111109)); $this->assertSame('99943326', $tfa->getCode($secret, 1111111111)); diff --git a/testsDependency/BaconQRCodeTest.php b/testsDependency/BaconQRCodeTest.php index 2a23061..69d8e7c 100644 --- a/testsDependency/BaconQRCodeTest.php +++ b/testsDependency/BaconQRCodeTest.php @@ -8,6 +8,7 @@ use RobThree\Auth\Algorithm; use RobThree\Auth\Providers\Qr\BaconQrCodeProvider; use RobThree\Auth\Providers\Qr\HandlesDataUri; +use RobThree\Auth\Providers\Qr\IQRCodeProvider; use RobThree\Auth\TwoFactorAuth; use RuntimeException; @@ -15,11 +16,17 @@ class BaconQRCodeTest extends TestCase { use HandlesDataUri; - public function testDependency(): void + protected IQRCodeProvider $qr; + + protected function setUp(): void { - $qr = new BaconQrCodeProvider(1, '#000', '#FFF', 'svg'); + $this->qr = new BaconQrCodeProvider(1, '#000', '#FFF', 'svg'); + ; + } - $tfa = new TwoFactorAuth('Test&Issuer', 6, 30, Algorithm::Sha1, $qr); + public function testDependency(): void + { + $tfa = new TwoFactorAuth($this->qr, 'Test&Issuer', 6, 30, Algorithm::Sha1); $data = $this->DecodeDataUri($tfa->getQRCodeImageAsDataUri('Test&Label', 'VMR466AB62ZBOKHE')); $this->assertSame('image/svg+xml', $data['mimetype']); diff --git a/testsDependency/EndroidQRCodeTest.php b/testsDependency/EndroidQRCodeTest.php index 6bd59fe..081021c 100644 --- a/testsDependency/EndroidQRCodeTest.php +++ b/testsDependency/EndroidQRCodeTest.php @@ -17,7 +17,7 @@ class EndroidQRCodeTest extends TestCase public function testDependency(): void { $qr = new EndroidQrCodeProvider(); - $tfa = new TwoFactorAuth('Test&Issuer', 6, 30, Algorithm::Sha1, $qr); + $tfa = new TwoFactorAuth($qr, 'Test&Issuer', 6, 30, Algorithm::Sha1); $data = $this->DecodeDataUri($tfa->getQRCodeImageAsDataUri('Test&Label', 'VMR466AB62ZBOKHE')); $this->assertSame('image/png', $data['mimetype']); $this->assertSame('base64', $data['encoding']);