Kévin Tessier 944fd76206 first commit | 6 years ago | |
---|---|---|
.. | ||
demo | 6 years ago | |
lib | 6 years ago | |
tests | 6 years ago | |
LICENSE | 6 years ago | |
README.md | 6 years ago | |
TwoFactorAuth.phpproj | 6 years ago | |
TwoFactorAuth.sln | 6 years ago | |
composer.json | 6 years ago | |
composer.lock | 6 years ago | |
logo.png | 6 years ago | |
multifactorauthforeveryone.png | 6 years ago |
PHP library for two-factor (or multi-factor) authentication using TOTP and QR-codes. Inspired by, based on but most importantly an improvement on 'PHPGangsta/GoogleAuthenticator'. There's a .Net implementation of this library as well.
GoogleQRCodeProvider
(default), QRServerProvider
or QRicketProvider
but you can also provide your own QR-code provider.Run the following command:
php composer.phar require robthree/twofactorauth
If you want to hit the ground running then have a look at the demo. It's very simple and easy!
Here are some code snippets that should help you get started...
// Create a TwoFactorAuth instance
$tfa = new RobThree\Auth\TwoFactorAuth('My Company');
The TwoFactorAuth class constructor accepts 7 parameters (all optional):
Parameter | Default value | Use |
---|---|---|
$issuer |
null |
Will be displayed in the app as issuer name |
$digits |
6 |
The number of digits the resulting codes will be |
$period |
30 |
The number of seconds a code will be valid |
$algorithm |
sha1 |
The algorithm used |
$qrcodeprovider |
null |
QR-code provider (more on this later) |
$rngprovider |
null |
Random Number Generator provider (more on this later) |
$timeprovider |
null |
Time provider (more on this later) |
These parameters are all 'write once
'; the class will, for it's lifetime, use these values when generating / calculating codes. The number of digits, the period and algorithm are all set to values Google's Authticator app uses (and supports). You may specify 8
digits, a period of 45
seconds and the sha256
algorithm but the authenticator app (be it Google's implementation, Authy or any other app) may or may not support these values. Your mileage may vary; keep it on the safe side if you don't control which app your audience uses.
When a user wants to setup two-factor auth (or, more correctly, multi-factor auth) you need to create a secret. This will be your shared secret. This secret will need to be entered by the user in their app. This can be done manually, in which case you simply display the secret and have the user type it in the app:
$secret = $tfa->createSecret();
The createSecret()
method accepts two arguments: $bits
(default: 80
) and $requirecryptosecure
(default: true
). The former is the number of bits generated for the shared secret. Make sure this argument is a multiple of 8 and, again, keep in mind that not all combinations may be supported by all apps. Google authenticator seems happy with 80 and 160, the default is set to 80 because that's what most sites (that I know of) currently use; however a value of 160 or higher is recommended (see RFC 4226 - Algorithm Requirements). The latter is used to ensure that the secret is cryptographically secure; if you don't care very much for cryptographically secure secrets you can specify false
and use a non-cryptographically secure RNG provider.
// Display shared secret
<p>Please enter the following code in your app: '<?php echo $secret; ?>'</p>
Another, more user-friendly, way to get the shared secret into the app is to generate a QR-code which can be scanned by the app. To generate these QR codes you can use any one of the built-in QRProvider
classes:
GoogleQRCodeProvider
(default)QRServerProvider
QRicketProvider
...or implement your own provider. To implement your own provider all you need to do is implement the IQRCodeProvider
interface. You can use the built-in providers mentioned before to serve as an example or read the next chapter in this file. The built-in classes all use a 3rd (e.g. external) party (Google, QRServer and QRicket) for the hard work of generating QR-codes (note: each of these services might at some point not be available or impose limitations to the number of codes generated per day, hour etc.). You could, however, easily use a project like PHP QR Code (or one of the many others) to generate your QR-codes without depending on external sources. Later on we'll demonstrate how to do this.
The built-in providers all have some provider-specific 'tweaks' you can 'apply'. Some provide support for different colors, others may let you specify the desired image-format etc. What they all have in common is that they return a QR-code as binary blob which, in turn, will be turned into a data URI by the TwoFactorAuth
class. This makes it easy for you to display the image without requiring extra 'roundtrips' from browser to server and vice versa.
// Display QR code to user
<p>Scan the following image with your app:</p>
<p><img src="<?php echo $tfa->getQRCodeImageAsDataUri('Bob Ross', $secret); ?>"></p>
When outputting a QR-code you can choose a $label
for the user (which, when entering a shared secret manually, will have to be chosen by the user). This label may be an empty string or null
. Also a $size
may be specified (in pixels, width == height) for which we use a default value of 200
.
When the shared secret is added to the app, the app will be ready to start generating codes which 'expire' each '$period
' number of seconds. To make sure the secret was entered, or scanned, correctly you need to verify this by having the user enter a generated code. To check if the generated code is valid you call the verifyCode()
method:
// Verify code
$result = $tfa->verifyCode($_SESSION['secret'], $_POST['verification']);
verifyCode()
will return either true
(the code was valid) or false
(the code was invalid; no points for you!). You may need to store $secret
in a $_SESSION
or other persistent storage between requests. The verifyCode()
accepts, aside from $secret
and $code
, two more parameters. The first being $discrepancy
. Since TOTP codes are based on time("slices") it is very important that the server (but also client) have a correct date/time. But because the two may differ a bit we usually allow a certain amount of leeway. Because generated codes are valid for a specific period (remember the $period
parameter in the TwoFactorAuth
's constructor?) we usually check the period directly before and the period directly after the current time when validating codes. So when the current time is 14:34:21
, which results in a 'current timeslice' of 14:34:00
to 14:34:30
we also calculate/verify the codes for 14:33:30
to 14:34:00
and for 14:34:30
to 14:35:00
. This gives us a 'window' of 14:33:30
to 14:35:00
. The $discrepancy
parameter specifies how many periods (or: timeslices) we check in either direction of the current time. The default $discrepancy
of 1
results in (max.) 3 period checks: -1, current and +1 period. A $discrepancy
of 4
would result in a larger window (or: bigger time difference between client and server) of -4, -3, -2, -1, current, +1, +2, +3 and +4 periods.
The second parameter $time
allows you to check a code for a specific point in time. This parameter has no real practical use but can be handy for unittesting etc. The default value, null
, means: use the current time.
$secret
with user and we're done!Ok, so now the code has been verified and found to be correct. Now we can store the $secret
with our user in our database (or elsewhere) and whenever the user begins a new session we ask for a code generated by the authentication app of their choice. All we need to do is call verifyCode()
again with the shared secret and the entered code and we know if the user is legit or not.
Simple as 1-2-3.
All we need is 3 methods and a constructor:
public function __construct(
$issuer = null,
$digits = 6,
$period = 30,
$algorithm = 'sha1',
RobThree\Auth\Providers\Qr\IQRCodeProvider $qrcodeprovider = null,
RobThree\Auth\Providers\Rng\IRNGProvider $rngprovider = null
);
public function createSecret($bits = 80, $requirecryptosecure = true): string;
public function getQRCodeImageAsDataUri($label, $secret, $size = 200): string;
public function verifyCode($secret, $code, $discrepancy = 1, $time = null): bool;
As mentioned before, this library comes with three 'built-in' QR-code providers. This chapter will touch the subject a bit but most of it should be self-explanatory. The TwoFactorAuth
-class accepts a $qrcodeprovider
parameter which lets you specify a built-in or custom QR-code provider. All three built-in providers do a simple HTTP request to retrieve an image using cURL and implement the IQRCodeProvider
interface which is all you need to implement to write your own QR-code provider.
The default provider is the GoogleQRCodeProvider
which uses the Google Chart Tools to render QR-codes. Then we have the QRServerProvider
which uses the goqr.me API and finally we have the QRicketProvider
which uses the QRickit API. All three inherit from a common (abstract) baseclass named BaseHTTPQRCodeProvider
because all three share the same functionality: retrieve an image from a 3rd party over HTTP. All three classes have constructors that allow you to tweak some settings and most, if not all, arguments should speak for themselves. If you're not sure which values are supported, click the links in this paragraph for documentation on the API's that are utilized by these classes.
If you don't like any of the built-in classes because you don't want to rely on external resources for example or because you're paranoid about sending the TOTP secret to these 3rd parties (which is useless to them since they miss at least one other factor in the MFA process), feel tree to implement your own. The IQRCodeProvider
interface couldn't be any simpler. All you need to do is implement 2 methods:
getMimeType();
getQRCodeImage($qrtext, $size);
The getMimeType()
method should return the MIME type of the image that is returned by our implementation of getQRCodeImage()
. In this example it's simply image/png
. The getQRCodeImage()
method is passed two arguments: $qrtext
and $size
. The latter, $size
, is simply the width/height in pixels of the image desired by the caller. The first, $qrtext
is the text that should be encoded in the QR-code. An example of such a text would be:
otpauth://totp/LABEL:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=ISSUER
All you need to do is return the QR-code as binary image data and you're done. All parts of the $qrtext
have been escaped for you (but note: you may need to escape the entire $qrtext
just once more when passing the data to another server as GET-parameter).
Let's see if we can use PHP QR Code to implement our own, custom, no-3rd-parties-allowed-here, provider. We start with downloading the required (single) file and putting it in the directory where TwoFactorAuth.php
is located as well. Now let's implement the provider: create another file named myprovider.php
in the Providers\Qr
directory and paste in this content:
<?php
require_once '../../phpqrcode.php'; // Yeah, we're gonna need that
namespace RobThree\Auth\Providers\Qr;
class MyProvider implements IQRCodeProvider {
public function getMimeType() {
return 'image/png'; // This provider only returns PNG's
}
public function getQRCodeImage($qrtext, $size) {
ob_start(); // 'Catch' QRCode's output
QRCode::png($qrtext, null, QR_ECLEVEL_L, 3, 4); // We ignore $size and set it to 3
// since phpqrcode doesn't support
// a size in pixels...
$result = ob_get_contents(); // 'Catch' QRCode's output
ob_end_clean(); // Cleanup
return $result; // Return image
}
}
That's it. We're done! We've implemented our own provider (with help of PHP QR Code). No more external dependencies, no more unnecessary latencies. Now let's use our provider:
<?php
$mp = new RobThree\Auth\Providers\Qr\MyProvider();
$tfa = new RobThree\Auth\TwoFactorAuth('My Company', 6, 30, 'sha1', $mp);
$secret = $tfa->createSecret();
?>
<p><img src="<?php echo $tfa->getQRCodeImageAsDataUri('Bob Ross', $secret); ?>"></p>
Voilà. Couldn't make it any simpler.
This library also comes with three 'built-in' RNG providers (Random Number Generator). The RNG provider generates a number of random bytes and returns these bytes as a string. These values are then used to create the secret. By default (no RNG provider specified) TwoFactorAuth will try to determine the best available RNG provider to use. It will, by default, try to use the CSRNGProvider
for PHP7+ or the MCryptRNGProvider
; if this is not available/supported for any reason it will try to use the OpenSSLRNGProvider
and if that is also not available/supported it will try to use the final RNG provider: HashRNGProvider
. Each of these providers use their own method of generating a random sequence of bytes. The first three (CSRNGProvider
, OpenSSLRNGProvider
and MCryptRNGProvider
) return a cryptographically secure sequence of random bytes whereas the HashRNGProvider
returns a non-cryptographically secure sequence.
You can easily implement your own RNGProvider
by simply implementing the IRNGProvider
interface. Each of the 'built-in' RNG providers have some constructor parameters that allow you to 'tweak' some of the settings to use when creating the random bytes such as which source to use (MCryptRNGProvider
) or which hashing algorithm (HashRNGProvider
). I encourage you to have a look at some of the 'built-in' RNG providers for details and the IRNGProvider
interface.
Another set of providers in this library are the Time Providers; this library provides three 'built-in' ones. The default Time Provider used is the LocalMachineTimeProvider
; this provider simply returns the output of Time()
and is highly recommended as default provider. The HttpTimeProvider
executes a HEAD
request against a given webserver (default: google.com) and tries to extract the Date:
-HTTP header and returns it's date. Other url's/domains can be used by specifying the url in the constructor. The final Time Provider is the ConvertUnixTimeDotComTimeProvider
which does a HTTP request to convert-unix-time.com/api
and decodes the JSON
result to retrieve the time.
You can easily implement your own TimeProvider
by simply implementing the ITimeProvider
interface.
As to why these Time Providers are implemented: it allows the TwoFactorAuth library to ensure the hosts time is correct (or rather: within a margin). You can use the ensureCorrectTime()
method to ensure the hosts time is correct. By default this method will compare the hosts time (returned by calling time()
on the LocalMachineTimeProvider
) to Google's and convert-unix-time.com's current time. You can pass an array of ITimeProvider
s and specify the leniency
(second argument) allowed (default: 5 seconds). The method will throw when the TwoFactorAuth's timeprovider (which can be any ITimeProvider
, see constructor) differs more than the given amount of seconds from any of the given ITimeProviders
. We advise to call this method sparingly when relying on 3rd parties (which both the HttpTimeProvider
and ConvertUnixTimeDotComTimeProvider
do) or, if you need to ensure time is correct on a (very) regular basis to implement an ITimeProvider
that is more efficient than the 'built-in' ones (like use a GPS signal). The ensureCorrectTime()
method is mostly to be used to make sure the server is configured correctly.
Licensed under MIT license. See LICENSE for details.
Logo / icon under CC0 1.0 Universal (CC0 1.0) Public Domain Dedication (Archived page)