make QR Code Provider a mandatory constructor argument

This change is discussed in #104
Currently, the library defaults to a QR Code Provider using an external
service, thus leaking secrets.

This change forces the definition of a QR Code Provider in the
constructor. It is a breaking change.

fixes #104
This commit is contained in:
Nicolas CARPi
2024-04-25 21:23:39 +02:00
parent cabcf5d493
commit 2080319f6f
10 changed files with 99 additions and 67 deletions
+17 -4
View File
@@ -17,18 +17,31 @@ or if you have composer installed globally
composer require robthree/twofactorauth
```
**Note:** If you are not using a composer-aware framework, you should [include the composer loader yourself](https://getcomposer.org/doc/01-basic-usage.md#autoloading).
## 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](docs/qr-codes.md) for more information about the different possibilites.
Example code:
```php
use RobThree\Auth\TwoFactorAuth;
use RobThree\Auth\Providers\Qr\BaconQrCodeProvider; // if using Bacon
use RobThree\Auth\Providers\Qr\EndroidQrCodeProvider; // if using Endroid
$tfa = new TwoFactorAuth();
// using Bacon
$tfa = new TwoFactorAuth(new BaconQrCodeProvider());
// using Endroid
$tfa = new TwoFactorAuth(new EndroidQrCodeProvider());
// using a custom object
$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.
+22 -22
View File
@@ -18,16 +18,6 @@ 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 +28,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;
$qrCodeProvider = new YourChosenProvider();
$tfa = new TwoFactorAuth(
issuer: "Your Company Or App Name",
qrcodeprovider: $qrCodeProvider
);
// using a custom object implementing IQRCodeProvider
$tfa = new TwoFactorAuth(new MyQrCodeProvider());
// using named argument and a variable
$tfa = new TwoFactorAuth(qrcodeprovider: $qrGenerator);
```
As you create a new instance of your provider, you can supply any extra configuration there.
## Online Providers
**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.
* 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)
Example:
```php
use RobThree\Auth\TwoFactorAuth;
use RobThree\Auth\Providers\Qr\GoogleChartsQrCodeProvider;
$tfa = new TwoFactorAuth(new GoogleChartsQrCodeProvider());
```
+15
View File
@@ -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'`
+3 -11
View File
@@ -7,7 +7,6 @@ namespace RobThree\Auth;
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 @@ class TwoFactorAuth
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 @@ class TwoFactorAuth
. '&digits=' . $this->digits;
}
public function getQrCodeProvider(): IQRCodeProvider
{
// Set default QR Code provider if none was specified
return $this->qrcodeprovider ??= new QRServerProvider();
}
/**
* @throws TwoFactorAuthException
*/
+11 -9
View File
@@ -7,6 +7,7 @@ namespace Tests\Providers\Qr;
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;
protected IQRCodeProvider $qr;
protected function setUp(): void
{
$this->qr = new TestQrProvider();
}
public function testTotpUriIsCorrect(): void
{
$qr = new TestQrProvider();
$tfa = new TwoFactorAuth('Test&Issuer', 6, 30, Algorithm::Sha1, $qr);
$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 @@ class IQRCodeProviderTest extends TestCase
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 @@ class IQRCodeProviderTest extends TestCase
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);
+2 -1
View File
@@ -7,12 +7,13 @@ namespace Tests\Providers\Rng;
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());
}
}
+4 -3
View File
@@ -8,6 +8,7 @@ use PHPUnit\Framework\TestCase;
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 @@ class ITimeProviderTest extends TestCase
$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 @@ class ITimeProviderTest extends TestCase
$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 @@ class ITimeProviderTest extends TestCase
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
}
}
+14 -13
View File
@@ -11,6 +11,7 @@ use RobThree\Auth\Providers\Time\HttpTimeProvider;
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 @@ class TwoFactorAuthTest extends TestCase
{
$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 @@ class TwoFactorAuthTest extends TestCase
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 @@ class TwoFactorAuthTest extends TestCase
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 @@ class TwoFactorAuthTest extends TestCase
public function testGetCodeThrowsOnInvalidBase32String1(): void
{
$tfa = new TwoFactorAuth('Test');
$tfa = new TwoFactorAuth(new TestQrProvider(), 'Test');
$this->expectException(TwoFactorAuthException::class);
@@ -104,7 +105,7 @@ class TwoFactorAuthTest extends TestCase
public function testGetCodeThrowsOnInvalidBase32String2(): void
{
$tfa = new TwoFactorAuth('Test');
$tfa = new TwoFactorAuth(new TestQrProvider(), 'Test');
$this->expectException(TwoFactorAuthException::class);
@@ -124,7 +125,7 @@ class TwoFactorAuthTest extends TestCase
// "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 @@ class TwoFactorAuthTest extends TestCase
// 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 @@ class TwoFactorAuthTest extends TestCase
{
//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 @@ class TwoFactorAuthTest extends TestCase
{
//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 @@ class TwoFactorAuthTest extends TestCase
{
//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));
+10 -3
View File
@@ -8,6 +8,7 @@ use PHPUnit\Framework\TestCase;
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;
protected IQRCodeProvider $qr;
protected function setUp(): void
{
$this->qr = new BaconQrCodeProvider(1, '#000', '#FFF', 'svg');
;
}
public function testDependency(): void
{
$qr = new BaconQrCodeProvider(1, '#000', '#FFF', 'svg');
$tfa = new TwoFactorAuth('Test&Issuer', 6, 30, Algorithm::Sha1, $qr);
$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']);
+1 -1
View File
@@ -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']);