mirror of
https://github.com/RobThree/TwoFactorAuth.git
synced 2026-08-20 06:42:45 +00:00
Compare commits
166 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 27cd1e1392 | |||
| 0159e77743 | |||
| 30248a8fb5 | |||
| d6e5e2ef87 | |||
| ab76ac71a4 | |||
| 023bfc16f6 | |||
| afb5cb09be | |||
| 1d628527e5 | |||
| d4528f58fe | |||
| e6e5d59297 | |||
| a968dd392a | |||
| 86338cf3cd | |||
| dc4e99e8c3 | |||
| 656e966cdf | |||
| 921425d0ff | |||
| 4711674ec0 | |||
| e049285b46 | |||
| b52655b803 | |||
| ad89250a8f | |||
| f034bc782e | |||
| f8ba3b234b | |||
| db6256b5e8 | |||
| 041d0e38e2 | |||
| 1e75674a45 | |||
| 25f463d19b | |||
| aaf24a66bd | |||
| 75d8955967 | |||
| 7e067166aa | |||
| 58a9628a57 | |||
| 887f261c88 | |||
| 225216a616 | |||
| 79988ef58a | |||
| f1e73aab3a | |||
| db0515e826 | |||
| 4be83550c0 | |||
| ac721e57b9 | |||
| a8c297c178 | |||
| aeb4b00c60 | |||
| ba4e8c55ed | |||
| 65681de5a3 | |||
| dc681e1ced | |||
| 5f2611cde2 | |||
| 5d36d4feb0 | |||
| cb45226800 | |||
| 2d1ec8d265 | |||
| ae4da10ff1 | |||
| 66e1e030ba | |||
| e76f31e93b | |||
| 4203749bd2 | |||
| 4f1543f782 | |||
| 8a220fe37b | |||
| 8a5cb24967 | |||
| dc9f168746 | |||
| 0096cce02d | |||
| 042f347666 | |||
| 5afcb45282 | |||
| b79d438032 | |||
| 65b17bca59 | |||
| c01202bc10 | |||
| 0453a94349 | |||
| 49f681e905 | |||
| 08846488cc | |||
| e15885ce9d | |||
| f006e63cff | |||
| 1faaf71391 | |||
| 148e409a38 | |||
| a627b889af | |||
| b24204bbed | |||
| 30a38627ae | |||
| b99f4f96d0 | |||
| 38f349fccd | |||
| 3e9d402a6e | |||
| 33a32cb099 | |||
| 452d31bfbf | |||
| 58d3354367 | |||
| 2aa6f46e20 | |||
| 14e90c3faa | |||
| df74a59b43 | |||
| 3640fee09a | |||
| 2a2b91023c | |||
| bbf24892db | |||
| 9011515f87 | |||
| 1b7197f1b3 | |||
| 2c2afdb49b | |||
| 8b5c61cc1d | |||
| d86c73ebff | |||
| 178f4447a7 | |||
| 5957248509 | |||
| 7d21cee5c1 | |||
| cc2ae19bcd | |||
| 94571a257a | |||
| ee9bf04ab7 | |||
| c3f3c0a849 | |||
| 2060811d88 | |||
| c3110d9760 | |||
| 4efb71d0ba | |||
| b5f9eda499 | |||
| 4678ce91a2 | |||
| 2daae60a34 | |||
| 44c06ba856 | |||
| 9007489436 | |||
| f0136c9458 | |||
| de411a22eb | |||
| 7c0f0b3d7f | |||
| 4da0739b38 | |||
| 37983bf675 | |||
| 1a941f0c86 | |||
| c6f1f47481 | |||
| 45bdc41597 | |||
| 957128bcfe | |||
| 3407c33775 | |||
| 401fc07652 | |||
| b5cd72a00a | |||
| 7477d5d656 | |||
| f5f58a4c62 | |||
| 718160e171 | |||
| c18ec155ae | |||
| 75c49351a6 | |||
| e74f7a4bf6 | |||
| fca87f2d09 | |||
| 72572c5c3a | |||
| a813bf7ede | |||
| 39db2654a6 | |||
| 4ac2670429 | |||
| 5de91c2837 | |||
| 0ac68f6b86 | |||
| fac4ebd44b | |||
| ad8b7ab3e8 | |||
| d7e8ad8e23 | |||
| b8befc5aff | |||
| 37fbb99ac5 | |||
| ea9b87fe32 | |||
| 4f2364fef4 | |||
| e4ec94e14d | |||
| de210192a7 | |||
| f25b910be9 | |||
| b591c879a1 | |||
| 4408ce7884 | |||
| 83ad9fb4e4 | |||
| 93fc6355d4 | |||
| a77e7d8223 | |||
| 673223a2fc | |||
| d450ba432d | |||
| d22329375f | |||
| b1ecc9afbc | |||
| c9bfe0519a | |||
| 404f147aa7 | |||
| 18888f83ee | |||
| 3d04717346 | |||
| 5093ab230c | |||
| f90bc37319 | |||
| 0ebe1d69a6 | |||
| 78a2d6e514 | |||
| 582a26749a | |||
| c114772d01 | |||
| 9735116635 | |||
| 178c60d947 | |||
| b3bb9685ea | |||
| d03e976891 | |||
| 0b5d455b27 | |||
| aac54360a9 | |||
| a73f119a58 | |||
| 8a726eda5d | |||
| fa781c14e3 | |||
| ab6b059df5 | |||
| aec91881bf |
@@ -0,0 +1,4 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [RobThree]
|
||||
custom: ["https://paypal.me/robiii"]
|
||||
@@ -0,0 +1,30 @@
|
||||
name: Test Bacon QR Code Provider
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.1', '8.2']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
tools: composer
|
||||
coverage: xdebug
|
||||
ini-values: error_reporting=E_ALL
|
||||
|
||||
- uses: ramsey/composer-install@v2
|
||||
|
||||
- run: composer require bacon/bacon-qr-code
|
||||
|
||||
- run: composer lint-ci
|
||||
- run: composer test testsDependency/BaconQRCodeTest.php
|
||||
@@ -0,0 +1,31 @@
|
||||
name: Test Endroid QR Code Provider
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.1', '8.2']
|
||||
endroid-version: ["^4"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
tools: composer
|
||||
coverage: xdebug
|
||||
ini-values: error_reporting=E_ALL
|
||||
|
||||
- uses: ramsey/composer-install@v2
|
||||
|
||||
- run: composer require endroid/qrcode:${{ matrix.endroid-version }}
|
||||
|
||||
- run: composer lint-ci
|
||||
- run: composer test testsDependency/EndroidQRCodeTest.php
|
||||
@@ -0,0 +1,29 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.1', '8.2']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
tools: composer
|
||||
coverage: xdebug
|
||||
ini-values: error_reporting=E_ALL
|
||||
|
||||
- uses: ramsey/composer-install@v2
|
||||
|
||||
- run: composer lint-ci
|
||||
- run: composer phpstan
|
||||
- run: composer test
|
||||
+11
-1
@@ -125,7 +125,7 @@ publish/
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# TODO: Comment the next line if you want to checkin your web deploy settings
|
||||
# TODO: Comment the next line if you want to checkin your web deploy settings
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
@@ -181,3 +181,13 @@ UpgradeLog*.htm
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# Composer
|
||||
/vendor
|
||||
composer.lock
|
||||
|
||||
# .vs
|
||||
.vs/
|
||||
|
||||
.phpunit.result.cache
|
||||
.php-cs-fixer.cache
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* PHP-CS-Fixer config for RobThree/TwoFactorAuth
|
||||
*/
|
||||
$finder = PhpCsFixer\Finder::create()
|
||||
->name('/\.php|\.php.dist$/')
|
||||
->exclude('build')
|
||||
->exclude('demo')
|
||||
->exclude('docs')
|
||||
->in(['lib', 'tests', 'testsDependency'])
|
||||
;
|
||||
|
||||
$config = new PhpCsFixer\Config();
|
||||
|
||||
return $config->setRules(array(
|
||||
'@PSR2' => true,
|
||||
'@PSR12' => true,
|
||||
'@PHP82Migration' => true,
|
||||
'array_syntax' => ['syntax' => 'long'],
|
||||
'class_attributes_separation' => true,
|
||||
'declare_strict_types' => true,
|
||||
'dir_constant' => true,
|
||||
'is_null' => true,
|
||||
'no_homoglyph_names' => true,
|
||||
'no_null_property_initialization' => true,
|
||||
'no_php4_constructor' => true,
|
||||
'no_unused_imports' => true,
|
||||
'no_useless_else' => true,
|
||||
'non_printable_character' => true,
|
||||
'ordered_imports' => true,
|
||||
'ordered_class_elements' => true,
|
||||
'php_unit_construct' => true,
|
||||
'pow_to_exponentiation' => true,
|
||||
'psr_autoloading' => true,
|
||||
'random_api_migration' => true,
|
||||
'return_assignment' => true,
|
||||
'self_accessor' => true,
|
||||
'semicolon_after_instruction' => true,
|
||||
'short_scalar_cast' => true,
|
||||
'simplified_null_return' => true,
|
||||
'single_blank_line_before_namespace' => true,
|
||||
'single_class_element_per_statement' => true,
|
||||
'single_line_comment_style' => true,
|
||||
'single_quote' => true,
|
||||
'space_after_semicolon' => true,
|
||||
'standardize_not_equals' => true,
|
||||
'strict_param' => true,
|
||||
'ternary_operator_spaces' => true,
|
||||
'trailing_comma_in_multiline' => true,
|
||||
'trim_array_spaces' => true,
|
||||
'unary_operator_spaces' => true,
|
||||
'global_namespace_import' => [
|
||||
'import_classes' => true,
|
||||
'import_functions' => true,
|
||||
'import_constants' => true,
|
||||
],
|
||||
))
|
||||
->setFinder($finder)
|
||||
->setRiskyAllowed(true)
|
||||
;
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
language: php
|
||||
|
||||
php:
|
||||
- 5.3
|
||||
- 5.4
|
||||
- 5.5
|
||||
- 5.6
|
||||
- 7
|
||||
- hhvm
|
||||
|
||||
script: phpunit --coverage-text tests
|
||||
@@ -0,0 +1,32 @@
|
||||
# RobThree\TwoFactorAuth changelog
|
||||
|
||||
# Version 2.x
|
||||
|
||||
## Breaking changes
|
||||
|
||||
### PHP Version
|
||||
|
||||
Version 2.x requires at least PHP 8.1.
|
||||
|
||||
### Constructor signature
|
||||
|
||||
With version 2.x, the `algorithm` parameter of `RobThree\Auth\TwoFactorAuth` constructor is now an `enum`.
|
||||
|
||||
On version 1.x:
|
||||
|
||||
~~~php
|
||||
use RobThree\Auth\TwoFactorAuth;
|
||||
|
||||
$lib = new TwoFactorAuth('issuer-name', 6, 30, 'sha1');
|
||||
~~~
|
||||
|
||||
On version 2.x, simple change the algorithm from a `string` to the correct `enum`:
|
||||
|
||||
~~~php
|
||||
use RobThree\Auth\TwoFactorAuth;
|
||||
use RobThree\Auth\Algorithm;
|
||||
|
||||
$lib = new TwoFactorAuth('issuer-name', 6, 30, Algorithm::Sha1);
|
||||
~~~
|
||||
|
||||
See the [Algorithm.php](./lib/Algorithm.php) file to see available algorithms.
|
||||
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2015 Rob Janssen
|
||||
Copyright (c) 2014-2021 Rob Janssen and contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#  PHP library for Two Factor Authentication
|
||||
|
||||
[](https://travis-ci.org/RobThree/TwoFactorAuth/) [](https://packagist.org/packages/robthree/twofactorauth) [](LICENSE) [](https://packagist.org/packages/robthree/twofactorauth) [](http://hhvm.h4cc.de/package/robthree/twofactorauth) [](https://codeclimate.com/github/RobThree/TwoFactorAuth) [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=6MB5M2SQLP636 "Keep me off the streets")
|
||||
[](https://github.com/RobThree/TwoFactorAuth/actions?query=branch%3Amaster) [](https://packagist.org/packages/robthree/twofactorauth) [](LICENSE) [](https://packagist.org/packages/robthree/twofactorauth) [](https://codeclimate.com/github/RobThree/TwoFactorAuth) [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=6MB5M2SQLP636 "Keep me off the streets")
|
||||
|
||||
PHP library for [two-factor (or multi-factor) authentication](http://en.wikipedia.org/wiki/Multi-factor_authentication) using [TOTP](http://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm) and [QR-codes](http://en.wikipedia.org/wiki/QR_code). Inspired by, based on but most importantly an *improvement* on '[PHPGangsta/GoogleAuthenticator](https://github.com/PHPGangsta/GoogleAuthenticator)'. There's a [.Net implementation](https://github.com/RobThree/TwoFactorAuth.Net) of this library as well.
|
||||
|
||||
@@ -10,172 +10,34 @@ PHP library for [two-factor (or multi-factor) authentication](http://en.wikipedi
|
||||
|
||||
## Requirements
|
||||
|
||||
* Tested on PHP 5.3, 5.4, 5.5 and 5.6, 7 and HHVM
|
||||
* [cURL](http://php.net/manual/en/book.curl.php) when using the provided `GoogleQRCodeProvider` (default), `QRServerProvider` or `QRicketProvider` but you can also provide your own QR-code provider.
|
||||
* [random_bytes()](http://php.net/manual/en/function.random-bytes.php), [MCrypt](http://php.net/manual/en/book.mcrypt.php), [OpenSSL](http://php.net/manual/en/book.openssl.php) or [Hash](http://php.net/manual/en/book.hash.php) depending on which built-in RNG you use (TwoFactorAuth will try to 'autodetect' and use the best available); however: feel free to provide your own (CS)RNG.
|
||||
* Requires PHP version >=8.1
|
||||
* [cURL](http://php.net/manual/en/book.curl.php) when using the provided `QRServerProvider` (default), `ImageChartsQRCodeProvider` or `QRicketProvider` but you can also provide your own QR-code provider.
|
||||
* [random_bytes()](http://php.net/manual/en/function.random-bytes.php), [OpenSSL](http://php.net/manual/en/book.openssl.php) or [Hash](http://php.net/manual/en/book.hash.php) depending on which built-in RNG you use (TwoFactorAuth will try to 'autodetect' and use the best available); however: feel free to provide your own (CS)RNG.
|
||||
|
||||
Optionally, you may need:
|
||||
|
||||
* [sockets](https://www.php.net/manual/en/book.sockets.php) if you are using `NTPTimeProvider`
|
||||
* [endroid/qr-code](https://github.com/endroid/qr-code) if using `EndroidQrCodeProvider` or `EndroidQrCodeWithLogoProvider`.
|
||||
* [bacon/bacon-qr-code](https://github.com/Bacon/BaconQrCode) if using `BaconQrCodeProvider`.
|
||||
|
||||
## Installation
|
||||
|
||||
Run the following command:
|
||||
The best way of installing this library is with composer:
|
||||
|
||||
`php composer.phar require robthree/twofactorauth`
|
||||
|
||||
## Quick start
|
||||
|
||||
If you want to hit the ground running then have a look at the [demo](demo/demo.php). It's very simple and easy!
|
||||
|
||||
## Usage
|
||||
|
||||
Here are some code snippets that should help you get started...
|
||||
For a quick start, have a look at the [getting started](https://robthree.github.io/TwoFactorAuth/getting-started.html) page or try out the [demo](demo/demo.php).
|
||||
|
||||
````php
|
||||
// Create a TwoFactorAuth instance
|
||||
$tfa = new RobThree\Auth\TwoFactorAuth('My Company');
|
||||
````
|
||||
|
||||
The TwoFactorAuth class constructor accepts 6 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)
|
||||
|
||||
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.
|
||||
|
||||
### Step 1: Set up secret shared key
|
||||
|
||||
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:
|
||||
|
||||
````php
|
||||
$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. 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.
|
||||
|
||||
````php
|
||||
// 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](http://en.wikipedia.org/wiki/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:
|
||||
|
||||
1. `GoogleQRCodeProvider` (default)
|
||||
2. `QRServerProvider`
|
||||
3. `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](http://phpqrcode.sourceforge.net/) (or one of the [many others](https://packagist.org/search/?q=qr)) to generate your QR-codes without depending on external sources. Later on we'll [demonstrate](#qr-code-providers) 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](http://en.wikipedia.org/wiki/Data_URI_scheme) 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.
|
||||
|
||||
````php
|
||||
// Display QR code to user
|
||||
<p>Scan the following image with your app:</p>
|
||||
<p><img src="<?php $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`.
|
||||
|
||||
### Step 2: Verify secret shared key
|
||||
|
||||
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:
|
||||
|
||||
````php
|
||||
// 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.
|
||||
|
||||
### Step 3: Store `$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:
|
||||
|
||||
````php
|
||||
__construct($issuer=null, $digits=6, $period=30, $algorithm='sha1', $qrcodeprovider=null, $rngprovider=null)
|
||||
createSecret($bits = 80, $requirecryptosecure = true)
|
||||
getQRCodeImageAsDataUri($label, $secret, $size = 200)
|
||||
verifyCode($secret, $code, $discrepancy = 1, $time = null)
|
||||
````
|
||||
|
||||
### QR-code providers
|
||||
|
||||
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`](lib/Providers/Qr/IQRCodeProvider.php) interface which is all you need to implement to write your own QR-code provider.
|
||||
|
||||
The default provider is the [`GoogleQRCodeProvider`](lib/Providers/Qr/GoogleQRCodeProvider.php) which uses the [Google Chart Tools](https://developers.google.com/chart/infographics/docs/qr_codes) to render QR-codes. Then we have the [`QRServerProvider`](lib/Providers/Qr/QRServerProvider.php) which uses the [goqr.me API](http://goqr.me/api/doc/create-qr-code/) and finally we have the [`QRicketProvider`](lib/Providers/Qr/QRicketProvider.php) which uses the [QRickit API](http://qrickit.com/qrickit_apps/qrickit_api.php). All three inherit from a common (abstract) baseclass named [`BaseHTTPQRCodeProvider`](lib/Providers/Qr/BaseHTTPQRCodeProvider.php) 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](http://en.wikipedia.org/wiki/Multi-factor_authentication)), feel tree to implement your own. The `IQRCodeProvider` interface couldn't be any simpler. All you need to do is implement 2 methods:
|
||||
|
||||
````php
|
||||
getMimeType();
|
||||
getQRCodeImage($qrtext, $size);
|
||||
````
|
||||
|
||||
The `getMimeType()` method should return the [MIME type](http://en.wikipedia.org/wiki/Internet_media_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](http://phpqrcode.sourceforge.net/) to implement our own, custom, no-3rd-parties-allowed-here, provider. We start with downloading the [required (single) file](https://github.com/t0k4rt/phpqrcode/blob/master/phpqrcode.php) 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
|
||||
<?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
|
||||
<?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 $tfa->getQRCodeImageAsDataUri('Bob Ross', $secret) ?>"></p>
|
||||
````
|
||||
|
||||
Voilà. Couldn't make it any simpler.
|
||||
|
||||
### RNG providers
|
||||
|
||||
This library also comes with three 'built-in' RNG providers ([Random Number Generator](https://en.wikipedia.org/wiki/Random_number_generation)). 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`](lib/Providers/Rng/CSRNGProvider.php) for PHP7+ or the [`MCryptRNGProvider`](lib/Providers/Rng/MCryptRNGProvider.php); if this is not available/supported for any reason it will try to use the [`OpenSSLRNGProvider`](lib/Providers/Rng/OpenSSLRNGProvider.php) and if that is also not available/supported it will try to use the final RNG provider: [`HashRNGProvider`](lib/Providers/Rng/HashRNGProvider.php). 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](https://en.wikipedia.org/wiki/Cryptographically_secure_pseudorandom_number_generator) 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](lib/Providers/Rng) for details and the [`IRNGProvider` interface](lib/Providers/Rng/IRNGProvider.php).
|
||||
If you need more in-depth information about the configuration available then you can read through the rest of [documentation](https://robthree.github.io/TwoFactorAuth).
|
||||
|
||||
## Integrations
|
||||
|
||||
- [CakePHP 3](https://github.com/andrej-griniuk/cakephp-two-factor-auth)
|
||||
- [CakePHP 3](https://github.com/andrej-griniuk/cakephp-two-factor-auth)
|
||||
|
||||
## License
|
||||
|
||||
Licensed under MIT license. See [LICENSE](https://raw.githubusercontent.com/RobThree/TwoFactorAuth/master/LICENSE) for details.
|
||||
Licensed under MIT license. See [LICENSE](./LICENSE) for details.
|
||||
|
||||
[Logo / icon](http://www.iconmay.com/Simple/Travel_and_Tourism_Part_2/luggage_lock_safety_baggage_keys_cylinder_lock_hotel_travel_tourism_luggage_lock_icon_465) under CC0 1.0 Universal (CC0 1.0) Public Domain Dedication ([Archived page](http://riii.nl/tm7ap))
|
||||
|
||||
+12
-3
@@ -14,6 +14,8 @@
|
||||
<PHPDevPort>41315</PHPDevPort>
|
||||
<PHPDevHostName>localhost</PHPDevHostName>
|
||||
<IISProjectUrl>http://localhost:41315/</IISProjectUrl>
|
||||
<Runtime>PHP</Runtime>
|
||||
<RuntimeVersion>8.1</RuntimeVersion>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<IncludeDebugInformation>true</IncludeDebugInformation>
|
||||
@@ -25,17 +27,21 @@
|
||||
<Compile Include="demo\demo.php" />
|
||||
<Compile Include="demo\loader.php" />
|
||||
<Compile Include="lib\Providers\Qr\BaseHTTPQRCodeProvider.php" />
|
||||
<Compile Include="lib\Providers\Qr\GoogleQRCodeProvider.php" />
|
||||
<Compile Include="lib\Providers\Qr\ImageChartsQRCodeProvider.php" />
|
||||
<Compile Include="lib\Providers\Qr\IQRCodeProvider.php" />
|
||||
<Compile Include="lib\Providers\Qr\QRException.php" />
|
||||
<Compile Include="lib\Providers\Qr\QRicketProvider.php" />
|
||||
<Compile Include="lib\Providers\Qr\QRServerProvider.php" />
|
||||
<Compile Include="lib\Providers\Rng\CSRNGProvider.php" />
|
||||
<Compile Include="lib\Providers\Rng\IRNGProvider.php" />
|
||||
<Compile Include="lib\Providers\Rng\MCryptRNGProvider.php" />
|
||||
<Compile Include="lib\Providers\Rng\OpenSSLRNGProvider.php" />
|
||||
<Compile Include="lib\Providers\Rng\HashRNGProvider.php" />
|
||||
<Compile Include="lib\Providers\Rng\RNGException.php" />
|
||||
<Compile Include="lib\Providers\Time\HttpTimeProvider.php" />
|
||||
<Compile Include="lib\Providers\Time\ITimeProvider.php" />
|
||||
<Compile Include="lib\Providers\Time\LocalMachineTimeProvider.php" />
|
||||
<Compile Include="lib\Providers\Time\NTPTimeProvider.php" />
|
||||
<Compile Include="lib\Providers\Time\TimeException.php" />
|
||||
<Compile Include="lib\TwoFactorAuth.php" />
|
||||
<Compile Include=".gitignore" />
|
||||
<Compile Include="README.md" />
|
||||
@@ -45,6 +51,7 @@
|
||||
<ItemGroup>
|
||||
<Folder Include="lib\" />
|
||||
<Folder Include="lib\Providers\" />
|
||||
<Folder Include="lib\Providers\Time\" />
|
||||
<Folder Include="lib\Providers\Qr\" />
|
||||
<Folder Include="lib\Providers\Rng\" />
|
||||
<Folder Include="demo\" />
|
||||
@@ -53,8 +60,10 @@
|
||||
<ItemGroup>
|
||||
<Content Include=".travis.yml" />
|
||||
<Content Include="composer.json" />
|
||||
<Content Include="composer.lock" />
|
||||
<Content Include="logo.png" />
|
||||
<Content Include="multifactorauthforeveryone.png" />
|
||||
<Content Include="LICENSE" />
|
||||
<Content Include="phpunit.xml" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
+57
-5
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"name": "robthree/twofactorauth",
|
||||
"description": "Two Factor Authentication",
|
||||
"version": "1.5",
|
||||
"type": "library",
|
||||
"keywords": [ "Authentication", "Two Factor Authentication", "Multi Factor Authentication", "TFA", "MFA", "PHP", "Authenticator", "Authy" ],
|
||||
"homepage": "https://github.com/RobThree/TwoFactorAuth",
|
||||
@@ -11,18 +10,71 @@
|
||||
"name": "Rob Janssen",
|
||||
"homepage": "http://robiii.me",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Nicolas CARPi",
|
||||
"homepage": "https://github.com/NicolasCARPi",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Will Power",
|
||||
"homepage": "https://github.com/willpower232",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/RobThree/TwoFactorAuth/issues",
|
||||
"source": "https://github.com/RobThree/TwoFactorAuth"
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9",
|
||||
"friendsofphp/php-cs-fixer": "^3.13",
|
||||
"phpstan/phpstan": "^1.9"
|
||||
},
|
||||
"suggest": {
|
||||
"bacon/bacon-qr-code": "Needed for BaconQrCodeProvider provider",
|
||||
"endroid/qr-code": "Needed for EndroidQrCodeProvider"
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"lib/"
|
||||
"psr-4": {
|
||||
"RobThree\\Auth\\": "lib"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"phpstan": [
|
||||
"phpstan analyze --xdebug lib tests testsDependency"
|
||||
],
|
||||
"lint": [
|
||||
"php-cs-fixer fix -v"
|
||||
],
|
||||
"lint-ci": [
|
||||
"PHP_CS_FIXER_IGNORE_ENV=1 php-cs-fixer fix -v --dry-run --stop-on-violation"
|
||||
],
|
||||
"test": [
|
||||
"XDEBUG_MODE=coverage phpunit"
|
||||
]
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.0"
|
||||
"archive": {
|
||||
"exclude": [
|
||||
"/.github/",
|
||||
"/demo/",
|
||||
"/docs/",
|
||||
"/tests/",
|
||||
"/testsDependency/",
|
||||
"/.gitignore",
|
||||
"/logo.png",
|
||||
"/multifactorauthforeveryone.png",
|
||||
"/phpunit.xml",
|
||||
"/TwoFactorAuth.phpproj",
|
||||
"/TwoFactorAuth.sln"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+37
-13
@@ -6,22 +6,46 @@
|
||||
<body>
|
||||
<ol>
|
||||
<?php
|
||||
require_once 'loader.php';
|
||||
Loader::register('../lib','RobThree\\Auth');
|
||||
|
||||
use \RobThree\Auth\TwoFactorAuth;
|
||||
// in practice you would require the composer loader if it was not already part of your framework or project
|
||||
spl_autoload_register(function ($className) {
|
||||
include_once str_replace(array('RobThree\\Auth', '\\'), array(__DIR__.'/../lib', '/'), $className) . '.php';
|
||||
});
|
||||
|
||||
$tfa = new TwoFactorAuth('MyApp');
|
||||
|
||||
echo '<li>First create a secret and associate it with a user';
|
||||
$secret = $tfa->createSecret();
|
||||
echo '<li>Next create a QR code and let the user scan it:<br><img src="' . $tfa->getQRCodeImageAsDataUri('My label', $secret) . '"><br>...or display the secret to the user for manual entry: ' . chunk_split($secret, 4, ' ');
|
||||
$code = $tfa->getCode($secret);
|
||||
echo '<li>Next, have the user verify the code; at this time the code displayed by a 2FA-app would be: <span style="color:#00c">' . $code . '</span> (but that changes periodically)';
|
||||
echo '<li>When the code checks out, 2FA can be / is enabled; store (encrypted?) secret with user and have the user verify a code each time a new session is started.';
|
||||
echo '<li>When aforementioned code (' . $code . ') was entered, the result would be: ' . (($tfa->verifyCode($secret, $code) === true) ? '<span style="color:#0c0">OK</span>' : '<span style="color:#c00">FAIL</span>');
|
||||
// substitute your company or app name here
|
||||
$tfa = new RobThree\Auth\TwoFactorAuth('RobThree TwoFactorAuth');
|
||||
?>
|
||||
<li>First create a secret and associate it with a user</li>
|
||||
<?php
|
||||
$secret = $tfa->createSecret();
|
||||
?>
|
||||
<li>
|
||||
Next create a QR code and let the user scan it:<br>
|
||||
<img src="<?php echo $tfa->getQRCodeImageAsDataUri('Demo', $secret); ?>"><br>
|
||||
...or display the secret to the user for manual entry:
|
||||
<?php echo chunk_split($secret, 4, ' '); ?>
|
||||
</li>
|
||||
<?php
|
||||
$code = $tfa->getCode($secret);
|
||||
?>
|
||||
<li>Next, have the user verify the code; at this time the code displayed by a 2FA-app would be: <span style="color:#00c"><?php echo $code; ?></span> (but that changes periodically)</li>
|
||||
<li>When the code checks out, 2FA can be / is enabled; store (encrypted?) secret with user and have the user verify a code each time a new session is started.</li>
|
||||
<li>
|
||||
When aforementioned code (<?php echo $code; ?>) was entered, the result would be:
|
||||
<?php if ($tfa->verifyCode($secret, $code) === true) { ?>
|
||||
<span style="color:#0c0">OK</span>
|
||||
<?php } else { ?>
|
||||
<span style="color:#c00">FAIL</span>
|
||||
<?php } ?>
|
||||
</li>
|
||||
</ol>
|
||||
<p>Note: Make sure your server-time is <a href="http://en.wikipedia.org/wiki/Network_Time_Protocol">NTP-synced</a>! Depending on the $discrepancy allowed your time cannot drift too much from the users' time!</p>
|
||||
<?php
|
||||
try {
|
||||
$tfa->ensureCorrectTime();
|
||||
echo 'Your hosts time seems to be correct / within margin';
|
||||
} catch (RobThree\Auth\TwoFactorAuthException $ex) {
|
||||
echo '<b>Warning:</b> Your hosts time seems to be off: ' . $ex->getMessage();
|
||||
}
|
||||
?>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
<?php
|
||||
|
||||
//http://www.leaseweblabs.com/2014/04/psr-0-psr-4-autoloading-classes-php/
|
||||
class Loader
|
||||
{
|
||||
protected static $parentPath = null;
|
||||
protected static $paths = null;
|
||||
protected static $files = null;
|
||||
protected static $nsChar = '\\';
|
||||
protected static $initialized = false;
|
||||
|
||||
protected static function initialize()
|
||||
{
|
||||
if (static::$initialized) return;
|
||||
static::$initialized = true;
|
||||
static::$parentPath = __FILE__;
|
||||
for ($i=substr_count(get_class(), static::$nsChar);$i>=0;$i--) {
|
||||
static::$parentPath = dirname(static::$parentPath);
|
||||
}
|
||||
static::$paths = array();
|
||||
static::$files = array(__FILE__);
|
||||
}
|
||||
|
||||
public static function register($path,$namespace) {
|
||||
if (!static::$initialized) static::initialize();
|
||||
static::$paths[$namespace] = trim($path,DIRECTORY_SEPARATOR);
|
||||
}
|
||||
|
||||
public static function load($class) {
|
||||
if (class_exists($class,false)) return;
|
||||
if (!static::$initialized) static::initialize();
|
||||
|
||||
foreach (static::$paths as $namespace => $path) {
|
||||
if (!$namespace || $namespace.static::$nsChar === substr($class, 0, strlen($namespace.static::$nsChar))) {
|
||||
|
||||
$fileName = substr($class,strlen($namespace.static::$nsChar)-1);
|
||||
$fileName = str_replace(static::$nsChar, DIRECTORY_SEPARATOR, ltrim($fileName,static::$nsChar));
|
||||
$fileName = static::$parentPath.DIRECTORY_SEPARATOR.$path.DIRECTORY_SEPARATOR.$fileName.'.php';
|
||||
|
||||
if (file_exists($fileName)) {
|
||||
include $fileName;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
spl_autoload_register(array('Loader', 'load'));
|
||||
@@ -0,0 +1,3 @@
|
||||
theme: jekyll-theme-minimal
|
||||
|
||||
logo: https://raw.githubusercontent.com/RobThree/TwoFactorAuth/master/multifactorauthforeveryone.png
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
<a href="{{ site.baseurl }}">← contents</a>
|
||||
|
||||
<h1>{{ page.title }}</h1>
|
||||
|
||||
{{ content }}
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
---
|
||||
|
||||
@import "{{ site.theme }}";
|
||||
|
||||
// undo some of the theme to allow code samples to be wider
|
||||
header {
|
||||
padding-right: 0;
|
||||
}
|
||||
@media print, screen and (min-width: 961px) {
|
||||
header {
|
||||
border: 1px solid #e5e5e5;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 30px;
|
||||
margin-right: 30px;
|
||||
padding-top: 20px;
|
||||
position: static;
|
||||
text-align: center;
|
||||
}
|
||||
section {
|
||||
float: none;
|
||||
width: auto;
|
||||
}
|
||||
footer {
|
||||
float: none;
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
|
||||
// ensure code samples can be really wide
|
||||
.language-php.highlighter-rouge {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
// add missing consistency
|
||||
header img {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
// quick navigation hack needs some spacing
|
||||
section > a:first-child {
|
||||
display: block;
|
||||
margin-bottom:45px;
|
||||
}
|
||||
|
||||
// 100% width is treated like clear which makes it look bad
|
||||
table {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
// reset document block whatever so the bullets aren't disturbed by the float
|
||||
ul {
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
layout: post
|
||||
title: Getting Started
|
||||
---
|
||||
|
||||
## 1. Installation
|
||||
|
||||
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
|
||||
|
||||
```php
|
||||
use RobThree\Auth\TwoFactorAuth;
|
||||
|
||||
$tfa = new TwoFactorAuth();
|
||||
```
|
||||
|
||||
**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.
|
||||
|
||||
```php
|
||||
$secret = $tfa->createSecret();
|
||||
```
|
||||
|
||||
Once you have a secret, it can be communicated to the user however you wish.
|
||||
|
||||
```php
|
||||
<p>Please enter the following code in your app: '<?php echo $secret; ?>'</p>
|
||||
```
|
||||
|
||||
**Note:** until you have verified the user is able to use the secret properly, you should store the secret as part of the current session and not save the secret against your user record.
|
||||
|
||||
## 4. Verifying
|
||||
|
||||
Having provided the user with the secret, the best practice is to verify their authenticator app can create the appropriate code.
|
||||
|
||||
```php
|
||||
$result = $tfa->verifyCode($secret, $_POST['verification']);
|
||||
```
|
||||
|
||||
If `$result` is `true` then your user has been able to successfully record the `$secret` in their authenticator app and it has generated an appropriate code.
|
||||
|
||||
You can now save the `$secret` to your user record and use the same `verifyCode` method each time they log in.
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
layout: post
|
||||
title: Improved Code Verification
|
||||
---
|
||||
|
||||
When verifying codes that a user has entered, there are other optional arguments which can improve verification of the code.
|
||||
|
||||
```php
|
||||
$result = $tfa->verifyCode($secret, $_POST['verification'], $discrepancy, $time, &$timeslice);
|
||||
```
|
||||
|
||||
## Discrepancy (default 1)
|
||||
|
||||
As the codes that are generated and accepted are consistent within a certain time window (i.e. a timeslice, 30 seconds long by default), it is very important that the server (and the users authenticator app) have the correct time (and date).
|
||||
|
||||
The value of `$discrepancy` is the number of timeslices checked in **both** directions of the current one. So when the current time is `14:34:21`, the 'current timeslice' is `14:34:00` to `14:34:30`. If the default is left unchanged, we also verify the code against the timeslice of `14:33:30` to `14:34:00` and for `14:34:30` to `14:35:00`.
|
||||
|
||||
This should be sufficient for most cases however you can increase it if you wish. It would be unwise for this to be too high as it could allow a code to be valid for long enough that it could be used fraudulently.
|
||||
|
||||
## Time (default null)
|
||||
|
||||
The second, `$time`, allows you to check a code for a specific point in time. This argument has no real practical use but can be handy for unit testing. The default value, `null`, means: use the current time.
|
||||
|
||||
## Timeslice
|
||||
|
||||
`$timeslice` returns a value by reference. The value returned is the timeslice that matched the code (if any) or `0`.
|
||||
|
||||
You can store a timeslice alongside the secret and verify that any new timeslice is greater than the existing one.
|
||||
|
||||
i.e. if `verifyCode` returns true _and_ the returned timeslice is greater than the last used timeslice for this user/secret then this is the first time the code has been used and you should now store the higher timeslice to verify that the user.
|
||||
|
||||
This is an effective defense against a [replay attack](https://en.wikipedia.org/wiki/Replay_attack).
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
title: Contents
|
||||
---
|
||||
|
||||
## [The Basics - Getting Started](getting-started.html)
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
[QR Codes](qr-codes.html)
|
||||
- [QRServerProvider](qr-codes/qr-server.html)
|
||||
- [ImageChartsQRCodeProvider](qr-codes/image-charts.html)
|
||||
- [QRicketProvider](qr-codes/qrickit.html)
|
||||
- [EndroidQrCodeProvider](qr-codes/endroid.html) (and EndroidQrCodeWithLogoProvider)
|
||||
- [BaconQRCodeProvider](qr-codes/bacon.html)
|
||||
|
||||
[Improved Code Verification](improved-code-verification.html)
|
||||
|
||||
[Other Optional Configuration](optional-configuration.html)
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
layout: post
|
||||
title: Optional Configuration
|
||||
---
|
||||
|
||||
## Instance Configuration
|
||||
|
||||
The instance (`new TwoFactorAuth()`) can only be configured by the constructor with the following optional arguments
|
||||
|
||||
Argument | Default value | Use
|
||||
------------------|---------------|-----
|
||||
`$issuer` | `null` | Will be displayed in the users app as the default issuer name when using QR code to import the secret
|
||||
`$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 (one of `sha1`, `sha256`, `sha512`, `md5`)
|
||||
`$qrcodeprovider` | `null` | QR-code provider
|
||||
`$rngprovider` | `null` | Random Number Generator provider
|
||||
`$timeprovider` | `null` | Time provider
|
||||
|
||||
**Note:** the default values for `$digits`, `$period`, and `$algorithm` provide the widest variety of support amongst common authenticator apps such as Google Authenticator. If you choose to use different values for these arguments you will likely have to instruct your users to use a specific app which supports your chosen configuration.
|
||||
|
||||
### RNG providers
|
||||
|
||||
This library also comes with some [Random Number Generator (RNG)](https://en.wikipedia.org/wiki/Random_number_generation) providers. 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 in this order.
|
||||
|
||||
1. [CSRNGProvider](https://github.com/RobThree/TwoFactorAuth/blob/master/lib/Providers/Rng/CSRNGProvider.php) for PHP7+
|
||||
2. [OpenSSLRNGProvider](https://github.com/RobThree/TwoFactorAuth/blob/master/lib/Providers/Rng/OpenSSLRNGProvider.php) where openssl is available
|
||||
3. [HashRNGProvider](https://github.com/RobThree/TwoFactorAuth/blob/master/lib/Providers/Rng/HashRNGProvider.php) **non-cryptographically secure** fallback
|
||||
|
||||
Each of these RNG providers have some constructor arguments that allow you to tweak some of the settings to use when creating the random bytes.
|
||||
|
||||
You can also implement your own by implementing the [`IRNGProvider` interface](https://github.com/RobThree/TwoFactorAuth/blob/master/lib/Providers/Rng/IRNGProvider.php).
|
||||
|
||||
### Time providers
|
||||
|
||||
These allow the TwoFactorAuth library to ensure the servers time is correct (or at least 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 the default `NTPTimeProvider` and `HttpTimeProvider`.
|
||||
|
||||
**Note:** the `NTPTimeProvider` requires your PHP to have the ability to create sockets. If you do not have that ability and wish to use this function, you should pass an array with only an instance of `HttpTimeProvider`.
|
||||
|
||||
Alternatively, you can pass an array of classes that implement the [`ITimeProvider` interface](https://github.com/RobThree/TwoFactorAuth/blob/master/lib/Providers/Time/ITimeProvider.php) to change this and specify the second argument, leniency in seconds (default: 5). An exception will be thrown if the time difference is greater than the leniency.
|
||||
|
||||
Ordinarily, you should not need to monitor that the time on the server is correct in this way however if you choose to, we advise to call this method sparingly when relying on 3rd parties (which both the `HttpTimeProvider` and `NTPTimeProvider` 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 (making use of a GPS signal for example).
|
||||
|
||||
## Secret Configuration
|
||||
|
||||
Secrets can be optionally configured with the following optional arguments
|
||||
|
||||
Argument | Default value | Use
|
||||
-----------------------|---------------|-----
|
||||
`$bits` | `80` | The number of bits (related to the length of the secret)
|
||||
`$requirecryptosecure` | `true` | Whether you want to require a cryptographically secure source of random numbers
|
||||
|
||||
**Note:** as above, these values provide the widest variety of support amongst common authenticator apps however you may choose to increase the value of `$bits` (160 or higher is recommended, see [RFC 4226 - Algorithm Requirements](https://tools.ietf.org/html/rfc4226#section-4)) as long as it is set to a multiple of 8.
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
layout: post
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
````php
|
||||
<p>Scan the following image with your app:</p>
|
||||
<img src="<?php echo $tfa->getQRCodeImageAsDataUri('Bob Ross', $secret); ?>">
|
||||
````
|
||||
|
||||
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.html) (default)
|
||||
|
||||
[ImageChartsQRCodeProvider](qr-codes/image-charts.html)
|
||||
|
||||
[QRicketProvider](qr-codes/qrickit.html)
|
||||
|
||||
## Offline Providers
|
||||
|
||||
[EndroidQrCodeProvider](qr-codes/endroid.html) and EndroidQrCodeWithLogoProvider
|
||||
|
||||
[BaconQRCodeProvider](qr-codes/bacon.html)
|
||||
|
||||
**Note:** offline providers may have additional PHP requirements in order to function, you should study what is required before trying to make use of them.
|
||||
|
||||
## 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).
|
||||
|
||||
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.
|
||||
|
||||
```php
|
||||
use RobThree\Auth\TwoFactorAuth;
|
||||
|
||||
$qrCodeProvider = new YourChosenProvider();
|
||||
|
||||
$tfa = new TwoFactorAuth(
|
||||
null,
|
||||
6,
|
||||
30,
|
||||
'sha1',
|
||||
$qrCodeProvider
|
||||
);
|
||||
```
|
||||
|
||||
As you create a new instance of your provider, you can supply any extra configuration there.
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
layout: post
|
||||
title: bacon/bacon-qr-code
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
In order to use this provider, you will need to install the library at version 2 (or later) and its dependencies
|
||||
|
||||
```
|
||||
composer require bacon/bacon-qr-code ^2.0
|
||||
```
|
||||
|
||||
You will also need the PHP imagick extension **if** you aren't using the SVG format.
|
||||
|
||||
## Optional Configuration
|
||||
|
||||
Argument | Default value
|
||||
--------------------|---------------
|
||||
`$borderWidth` | `4`
|
||||
`$backgroundColour` | `'#ffffff'`
|
||||
`$foregroundColour` | `'#000000'`
|
||||
`$format` | `'png'`
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
layout: post
|
||||
title: endroid/qr-code
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
In order to use this provider, you will need to install the library at version 3 and its dependencies
|
||||
|
||||
```
|
||||
composer require endroid/qr-code ^3.0
|
||||
```
|
||||
|
||||
You will also need the PHP gd extension installing.
|
||||
|
||||
## Optional Configuration
|
||||
|
||||
Argument | Default value
|
||||
------------------------|---------------
|
||||
`$bgcolor` | `'ffffff'`
|
||||
`$color` | `'000000'`
|
||||
`$margin` | `0`
|
||||
`$errorcorrectionlevel` | `'H'`
|
||||
|
||||
## Logo
|
||||
|
||||
If you make use of `EndroidQrCodeWithLogoProvider` then you have access to the `setLogo` function on the provider so you may add a logo to the centre of your QR code.
|
||||
|
||||
```php
|
||||
use RobThree\Auth\Providers\Qr\EndroidQrCodeWithLogoProvider;
|
||||
|
||||
$qrCodeProvider = new EndroidQrCodeWithLogoProvider();
|
||||
|
||||
$qrCodeProvider->setLogo('/path/to/your/image');
|
||||
```
|
||||
|
||||
You can see how to also set the size of the logo in the [source code](https://github.com/RobThree/TwoFactorAuth/blob/master/lib/Providers/Qr/EndroidQrCodeWithLogoProvider.php).
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
layout: post
|
||||
title: Image-Charts
|
||||
---
|
||||
|
||||
## Optional Configuration
|
||||
|
||||
Argument | Default value
|
||||
------------------------|---------------
|
||||
`$verifyssl` | `false`
|
||||
`$errorcorrectionlevel` | `'L'`
|
||||
`$margin` | `4`
|
||||
|
||||
`$verifyssl` is used internally to help guarantee the security of the connection. It is possible that where you are running the code from will have problems verifying an SSL connection so if you know this is not the case, you can supply `true`.
|
||||
|
||||
The other parameters are passed to [Image-Charts](https://documentation.image-charts.com/qr-codes/) so you can refer to them for more detail on how the values are used.
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
layout: post
|
||||
title: QR Server
|
||||
---
|
||||
|
||||
## Optional Configuration
|
||||
|
||||
Argument | Default value
|
||||
------------------------|---------------
|
||||
`$verifyssl` | `false`
|
||||
`$errorcorrectionlevel` | `'L'`
|
||||
`$margin` | `4`
|
||||
`$qzone` | `1`
|
||||
`$bgcolor` | `'ffffff'`
|
||||
`$color` | `'000000'`
|
||||
`$format` | `'png'`
|
||||
|
||||
`$verifyssl` is used internally to help guarantee the security of the connection. It is possible that where you are running the code from will have problems verifying an SSL connection so if you know this is not the case, you can supply `true`.
|
||||
|
||||
The other parameters are passed to [goqr.me](http://goqr.me/api/doc/create-qr-code/) so you can refer to them for more detail on how the values are used.
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
layout: post
|
||||
title: QRickit
|
||||
---
|
||||
|
||||
## Optional Configuration
|
||||
|
||||
Argument | Default value
|
||||
------------------------|---------------
|
||||
`$errorcorrectionlevel` | `'L'`
|
||||
`$bgcolor` | `'ffffff'`
|
||||
`$color` | `'000000'`
|
||||
`$format` | `'png'`
|
||||
|
||||
The parameters are passed to [QRickit](http://qrickit.com/qrickit_apps/qrickit_api.php) so you can refer to them for more detail on how the values are used.
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth;
|
||||
|
||||
/**
|
||||
* List of supported cryptographic algorithms
|
||||
*/
|
||||
enum Algorithm: string
|
||||
{
|
||||
case Md5 = 'md5';
|
||||
case Sha1 = 'sha1';
|
||||
case Sha256 = 'sha256';
|
||||
case Sha512 = 'sha512';
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth\Providers\Qr;
|
||||
|
||||
use BaconQrCode\Renderer\Color\Rgb;
|
||||
use BaconQrCode\Renderer\Image\EpsImageBackEnd;
|
||||
use BaconQrCode\Renderer\Image\ImageBackEndInterface;
|
||||
use BaconQrCode\Renderer\Image\ImagickImageBackEnd;
|
||||
use BaconQrCode\Renderer\Image\SvgImageBackEnd;
|
||||
use BaconQrCode\Renderer\ImageRenderer;
|
||||
|
||||
use BaconQrCode\Renderer\RendererStyle\EyeFill;
|
||||
use BaconQrCode\Renderer\RendererStyle\Fill;
|
||||
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
|
||||
use BaconQrCode\Writer;
|
||||
use RuntimeException;
|
||||
|
||||
class BaconQrCodeProvider implements IQRCodeProvider
|
||||
{
|
||||
/**
|
||||
* Ensure we using the latest Bacon QR Code and specify default options
|
||||
*/
|
||||
public function __construct(
|
||||
private int $borderWidth = 4,
|
||||
private string|array $backgroundColour = '#ffffff',
|
||||
private string|array $foregroundColour = '#000000',
|
||||
private string $format = 'png',
|
||||
) {
|
||||
$this->backgroundColour = $this->handleColour($this->backgroundColour);
|
||||
$this->foregroundColour = $this->handleColour($this->foregroundColour);
|
||||
$this->format = strtolower($this->format);
|
||||
}
|
||||
|
||||
public function getMimeType(): string
|
||||
{
|
||||
switch ($this->format) {
|
||||
case 'png':
|
||||
return 'image/png';
|
||||
case 'gif':
|
||||
return 'image/gif';
|
||||
case 'jpg':
|
||||
case 'jpeg':
|
||||
return 'image/jpeg';
|
||||
case 'svg':
|
||||
return 'image/svg+xml';
|
||||
case 'eps':
|
||||
return 'application/postscript';
|
||||
}
|
||||
|
||||
throw new RuntimeException(sprintf('Unknown MIME-type: %s', $this->format));
|
||||
}
|
||||
|
||||
public function getQRCodeImage(string $qrText, int $size): string
|
||||
{
|
||||
switch ($this->format) {
|
||||
case 'svg':
|
||||
$backend = new SvgImageBackEnd();
|
||||
break;
|
||||
case 'eps':
|
||||
$backend = new EpsImageBackEnd();
|
||||
break;
|
||||
default:
|
||||
$backend = new ImagickImageBackEnd($this->format);
|
||||
}
|
||||
|
||||
$output = $this->getQRCodeByBackend($qrText, $size, $backend);
|
||||
|
||||
if ($this->format == 'svg') {
|
||||
$svg = explode("\n", $output);
|
||||
return $svg[1];
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract QR code generation function
|
||||
* providing colour changing support
|
||||
*/
|
||||
private function getQRCodeByBackend($qrText, $size, ImageBackEndInterface $backend)
|
||||
{
|
||||
$rendererStyleArgs = array($size, $this->borderWidth);
|
||||
|
||||
if (is_array($this->foregroundColour) && is_array($this->backgroundColour)) {
|
||||
$rendererStyleArgs = array_merge($rendererStyleArgs, array(
|
||||
null,
|
||||
null,
|
||||
Fill::withForegroundColor(
|
||||
new Rgb(...$this->backgroundColour),
|
||||
new Rgb(...$this->foregroundColour),
|
||||
new EyeFill(null, null),
|
||||
new EyeFill(null, null),
|
||||
new EyeFill(null, null)
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
$writer = new Writer(new ImageRenderer(
|
||||
new RendererStyle(...$rendererStyleArgs),
|
||||
$backend
|
||||
));
|
||||
|
||||
return $writer->writeString($qrText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure colour is an array of three values but also
|
||||
* accept a string and assume its a 3 or 6 character hex
|
||||
*/
|
||||
private function handleColour(array|string $colour): array|string
|
||||
{
|
||||
if (is_string($colour) && $colour[0] == '#') {
|
||||
$hexToRGB = function ($input) {
|
||||
// ensure input no longer has a # for more predictable division
|
||||
// PHP 8.1 does not like implicitly casting a float to an int
|
||||
$input = trim($input, '#');
|
||||
|
||||
if (strlen($input) != 3 && strlen($input) != 6) {
|
||||
throw new RuntimeException('Colour should be a 3 or 6 character value after the #');
|
||||
}
|
||||
|
||||
// split the array into three chunks
|
||||
$split = str_split($input, strlen($input) / 3);
|
||||
|
||||
// cope with three character hex reference
|
||||
if (strlen($input) == 3) {
|
||||
array_walk($split, function (&$character) {
|
||||
$character = str_repeat($character, 2);
|
||||
});
|
||||
}
|
||||
|
||||
// convert hex to rgb
|
||||
return array_map('hexdec', $split);
|
||||
};
|
||||
|
||||
return $hexToRGB($colour);
|
||||
}
|
||||
|
||||
if (is_array($colour) && count($colour) == 3) {
|
||||
return $colour;
|
||||
}
|
||||
|
||||
throw new RuntimeException('Invalid colour value');
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth\Providers\Qr;
|
||||
|
||||
abstract class BaseHTTPQRCodeProvider implements IQRCodeProvider
|
||||
{
|
||||
protected $verifyssl;
|
||||
protected bool $verifyssl;
|
||||
|
||||
protected function getContent($url)
|
||||
protected function getContent(string $url): string|bool
|
||||
{
|
||||
$curlhandle = curl_init();
|
||||
|
||||
|
||||
curl_setopt_array($curlhandle, array(
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
@@ -17,11 +19,11 @@ abstract class BaseHTTPQRCodeProvider implements IQRCodeProvider
|
||||
CURLOPT_DNS_CACHE_TIMEOUT => 10,
|
||||
CURLOPT_TIMEOUT => 10,
|
||||
CURLOPT_SSL_VERIFYPEER => $this->verifyssl,
|
||||
CURLOPT_USERAGENT => 'TwoFactorAuth'
|
||||
CURLOPT_USERAGENT => 'TwoFactorAuth',
|
||||
));
|
||||
$data = curl_exec($curlhandle);
|
||||
|
||||
|
||||
curl_close($curlhandle);
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+91
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth\Providers\Qr;
|
||||
|
||||
use Endroid\QrCode\Color\Color;
|
||||
use Endroid\QrCode\ErrorCorrectionLevel;
|
||||
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelHigh;
|
||||
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelInterface;
|
||||
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelLow;
|
||||
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelMedium;
|
||||
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelQuartile;
|
||||
use Endroid\QrCode\QrCode;
|
||||
use Endroid\QrCode\Writer\PngWriter;
|
||||
|
||||
class EndroidQrCodeProvider implements IQRCodeProvider
|
||||
{
|
||||
public $bgcolor;
|
||||
|
||||
public $color;
|
||||
|
||||
public $margin;
|
||||
|
||||
public $errorcorrectionlevel;
|
||||
|
||||
protected $endroid4 = false;
|
||||
|
||||
public function __construct($bgcolor = 'ffffff', $color = '000000', $margin = 0, $errorcorrectionlevel = 'H')
|
||||
{
|
||||
$this->endroid4 = method_exists(QrCode::class, 'create');
|
||||
|
||||
$this->bgcolor = $this->handleColor($bgcolor);
|
||||
$this->color = $this->handleColor($color);
|
||||
$this->margin = $margin;
|
||||
$this->errorcorrectionlevel = $this->handleErrorCorrectionLevel($errorcorrectionlevel);
|
||||
}
|
||||
|
||||
public function getMimeType(): string
|
||||
{
|
||||
return 'image/png';
|
||||
}
|
||||
|
||||
public function getQRCodeImage(string $qrtext, int $size): string
|
||||
{
|
||||
if (!$this->endroid4) {
|
||||
return $this->qrCodeInstance($qrtext, $size)->writeString();
|
||||
}
|
||||
|
||||
$writer = new PngWriter();
|
||||
return $writer->write($this->qrCodeInstance($qrtext, $size))->getString();
|
||||
}
|
||||
|
||||
protected function qrCodeInstance(string $qrtext, int $size): QrCode
|
||||
{
|
||||
$qrCode = new QrCode($qrtext);
|
||||
$qrCode->setSize($size);
|
||||
|
||||
$qrCode->setErrorCorrectionLevel($this->errorcorrectionlevel);
|
||||
$qrCode->setMargin($this->margin);
|
||||
$qrCode->setBackgroundColor($this->bgcolor);
|
||||
$qrCode->setForegroundColor($this->color);
|
||||
|
||||
return $qrCode;
|
||||
}
|
||||
|
||||
private function handleColor(string $color): Color
|
||||
{
|
||||
$split = str_split($color, 2);
|
||||
$r = hexdec($split[0]);
|
||||
$g = hexdec($split[1]);
|
||||
$b = hexdec($split[2]);
|
||||
|
||||
return $this->endroid4 ? new Color($r, $g, $b, 0) : array('r' => $r, 'g' => $g, 'b' => $b, 'a' => 0);
|
||||
}
|
||||
|
||||
private function handleErrorCorrectionLevel(string $level): ErrorCorrectionLevelInterface
|
||||
{
|
||||
switch ($level) {
|
||||
case 'L':
|
||||
return $this->endroid4 ? new ErrorCorrectionLevelLow() : ErrorCorrectionLevel::LOW();
|
||||
case 'M':
|
||||
return $this->endroid4 ? new ErrorCorrectionLevelMedium() : ErrorCorrectionLevel::MEDIUM();
|
||||
case 'Q':
|
||||
return $this->endroid4 ? new ErrorCorrectionLevelQuartile() : ErrorCorrectionLevel::QUARTILE();
|
||||
case 'H':
|
||||
default:
|
||||
return $this->endroid4 ? new ErrorCorrectionLevelHigh() : ErrorCorrectionLevel::HIGH();
|
||||
}
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth\Providers\Qr;
|
||||
|
||||
use Endroid\QrCode\Logo\Logo;
|
||||
use Endroid\QrCode\QrCode;
|
||||
use Endroid\QrCode\Writer\PngWriter;
|
||||
|
||||
class EndroidQrCodeWithLogoProvider extends EndroidQrCodeProvider
|
||||
{
|
||||
protected $logoPath;
|
||||
|
||||
protected $logoSize;
|
||||
|
||||
/**
|
||||
* Adds an image to the middle of the QR Code.
|
||||
* @param string $path Path to an image file
|
||||
* @param array|int $size Just the width, or [width, height]
|
||||
*/
|
||||
public function setLogo($path, $size = null)
|
||||
{
|
||||
$this->logoPath = $path;
|
||||
$this->logoSize = (array)$size;
|
||||
}
|
||||
|
||||
public function getQRCodeImage(string $qrtext, int $size): string
|
||||
{
|
||||
if (!$this->endroid4) {
|
||||
return $this->qrCodeInstance($qrtext, $size)->writeString();
|
||||
}
|
||||
|
||||
$logo = null;
|
||||
if ($this->logoPath) {
|
||||
$logo = Logo::create($this->logoPath);
|
||||
if ($this->logoSize) {
|
||||
$logo->setResizeToWidth($this->logoSize[0]);
|
||||
if (isset($this->logoSize[1])) {
|
||||
$logo->setResizeToHeight($this->logoSize[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
$writer = new PngWriter();
|
||||
return $writer->write($this->qrCodeInstance($qrtext, $size), $logo)->getString();
|
||||
}
|
||||
|
||||
protected function qrCodeInstance(string $qrtext, int $size): QrCode
|
||||
{
|
||||
$qrCode = parent::qrCodeInstance($qrtext, $size);
|
||||
|
||||
if (!$this->endroid4 && $this->logoPath) {
|
||||
$qrCode->setLogoPath($this->logoPath);
|
||||
if ($this->logoSize) {
|
||||
$qrCode->setLogoSize($this->logoSize[0], $this->logoSize[1] ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
return $qrCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth\Providers\Qr;
|
||||
|
||||
// https://developers.google.com/chart/infographics/docs/qr_codes
|
||||
class GoogleChartsQrCodeProvider extends BaseHTTPQRCodeProvider
|
||||
{
|
||||
public function __construct(protected bool $verifyssl = false, public string $errorcorrectionlevel = 'L', public int $margin = 4, public string $encoding = 'UTF-8')
|
||||
{
|
||||
}
|
||||
|
||||
public function getMimeType(): string
|
||||
{
|
||||
return 'image/png';
|
||||
}
|
||||
|
||||
public function getQRCodeImage(string $qrtext, int $size): string
|
||||
{
|
||||
return $this->getContent($this->getUrl($qrtext, $size));
|
||||
}
|
||||
|
||||
public function getUrl(string $qrtext, int $size): string
|
||||
{
|
||||
return 'https://chart.googleapis.com/chart'
|
||||
. '?chs=' . $size . 'x' . $size
|
||||
. '&chld=' . urlencode(strtoupper($this->errorcorrectionlevel) . '|' . $this->margin)
|
||||
. '&cht=' . 'qr'
|
||||
. '&choe=' . $this->encoding
|
||||
. '&chl=' . rawurlencode($qrtext);
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace RobThree\Auth\Providers\Qr;
|
||||
|
||||
// https://developers.google.com/chart/infographics/docs/qr_codes
|
||||
class GoogleQRCodeProvider extends BaseHTTPQRCodeProvider
|
||||
{
|
||||
public $errorcorrectionlevel;
|
||||
public $margin;
|
||||
|
||||
function __construct($verifyssl = false, $errorcorrectionlevel = 'L', $margin = 1)
|
||||
{
|
||||
if (!is_bool($verifyssl))
|
||||
throw new QRException('VerifySSL must be bool');
|
||||
|
||||
$this->verifyssl = $verifyssl;
|
||||
|
||||
$this->errorcorrectionlevel = $errorcorrectionlevel;
|
||||
$this->margin = $margin;
|
||||
}
|
||||
|
||||
public function getMimeType()
|
||||
{
|
||||
return 'image/png';
|
||||
}
|
||||
|
||||
public function getQRCodeImage($qrtext, $size)
|
||||
{
|
||||
return $this->getContent($this->getUrl($qrtext, $size));
|
||||
}
|
||||
|
||||
public function getUrl($qrtext, $size)
|
||||
{
|
||||
return 'https://chart.googleapis.com/chart?cht=qr'
|
||||
. '&chs=' . $size . 'x' . $size
|
||||
. '&chld=' . $this->errorcorrectionlevel . '|' . $this->margin
|
||||
. '&chl=' . rawurlencode($qrtext);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth\Providers\Qr;
|
||||
|
||||
use function base64_decode;
|
||||
use function preg_match;
|
||||
|
||||
trait HandlesDataUri
|
||||
{
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function DecodeDataUri(string $datauri): ?array
|
||||
{
|
||||
if (preg_match('/data:(?P<mimetype>[\w\.\-\+\/]+);(?P<encoding>\w+),(?P<data>.*)/', $datauri, $m) === 1) {
|
||||
return array(
|
||||
'mimetype' => $m['mimetype'],
|
||||
'encoding' => $m['encoding'],
|
||||
'data' => base64_decode($m['data'], true),
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth\Providers\Qr;
|
||||
|
||||
interface IQRCodeProvider
|
||||
{
|
||||
public function getQRCodeImage($qrtext, $size);
|
||||
public function getMimeType();
|
||||
}
|
||||
/**
|
||||
* Generate and return the QR code to embed in a web page
|
||||
*
|
||||
* @param string $qrtext the value to encode in the QR code
|
||||
* @param int $size the desired size of the QR code
|
||||
*
|
||||
* @return string file contents of the QR code
|
||||
*/
|
||||
public function getQRCodeImage(string $qrtext, int $size): string;
|
||||
|
||||
/**
|
||||
* Returns the appropriate mime type for the QR code
|
||||
* that will be generated
|
||||
*/
|
||||
public function getMimeType(): string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth\Providers\Qr;
|
||||
|
||||
/**
|
||||
* Use https://image-charts.com to provide a QR code
|
||||
*/
|
||||
class ImageChartsQRCodeProvider extends BaseHTTPQRCodeProvider
|
||||
{
|
||||
public function __construct(protected bool $verifyssl = false, public string $errorcorrectionlevel = 'L', public int $margin = 1)
|
||||
{
|
||||
}
|
||||
|
||||
public function getMimeType(): string
|
||||
{
|
||||
return 'image/png';
|
||||
}
|
||||
|
||||
public function getQRCodeImage(string $qrtext, int $size): string
|
||||
{
|
||||
return $this->getContent($this->getUrl($qrtext, $size));
|
||||
}
|
||||
|
||||
public function getUrl(string $qrtext, int $size): string
|
||||
{
|
||||
return 'https://image-charts.com/chart?cht=qr'
|
||||
. '&chs=' . ceil($size / 2) . 'x' . ceil($size / 2)
|
||||
. '&chld=' . $this->errorcorrectionlevel . '|' . $this->margin
|
||||
. '&chl=' . rawurlencode($qrtext);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth\Providers\Qr;
|
||||
|
||||
class QRException extends \Exception
|
||||
use RobThree\Auth\TwoFactorAuthException;
|
||||
|
||||
class QRException extends TwoFactorAuthException
|
||||
{
|
||||
function __construct($message = "", $code = 0, $exception = null)
|
||||
{
|
||||
parent::__construct($message, $code, $exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth\Providers\Qr;
|
||||
|
||||
// http://goqr.me/api/doc/create-qr-code/
|
||||
class QRServerProvider extends BaseHTTPQRCodeProvider
|
||||
/**
|
||||
* Use http://goqr.me/api/doc/create-qr-code/ to get QR code
|
||||
*/
|
||||
class QRServerProvider extends BaseHTTPQRCodeProvider
|
||||
{
|
||||
public $errorcorrectionlevel;
|
||||
public $margin;
|
||||
public $qzone;
|
||||
public $bgcolor;
|
||||
public $color;
|
||||
public $format;
|
||||
|
||||
function __construct($verifyssl = false, $errorcorrectionlevel = 'L', $margin = 4, $qzone = 1, $bgcolor = 'ffffff', $color = '000000', $format = 'png')
|
||||
public function __construct(protected bool $verifyssl = false, public string $errorcorrectionlevel = 'L', public int $margin = 4, public int $qzone = 1, public string $bgcolor = 'ffffff', public string $color = '000000', public string $format = 'png')
|
||||
{
|
||||
if (!is_bool($verifyssl))
|
||||
throw new QRException('VerifySSL must be bool');
|
||||
|
||||
$this->verifyssl = $verifyssl;
|
||||
|
||||
$this->errorcorrectionlevel = $errorcorrectionlevel;
|
||||
$this->margin = $margin;
|
||||
$this->qzone = $qzone;
|
||||
$this->bgcolor = $bgcolor;
|
||||
$this->color = $color;
|
||||
$this->format = $format;
|
||||
}
|
||||
|
||||
public function getMimeType()
|
||||
|
||||
public function getMimeType(): string
|
||||
{
|
||||
switch (strtolower($this->format))
|
||||
{
|
||||
case 'png':
|
||||
switch (strtolower($this->format)) {
|
||||
case 'png':
|
||||
return 'image/png';
|
||||
case 'gif':
|
||||
case 'gif':
|
||||
return 'image/gif';
|
||||
case 'jpg':
|
||||
case 'jpeg':
|
||||
case 'jpg':
|
||||
case 'jpeg':
|
||||
return 'image/jpeg';
|
||||
case 'svg':
|
||||
case 'svg':
|
||||
return 'image/svg+xml';
|
||||
case 'eps':
|
||||
case 'eps':
|
||||
return 'application/postscript';
|
||||
}
|
||||
throw new QRException(sprintf('Unknown MIME-type: %s', $this->format));
|
||||
}
|
||||
|
||||
public function getQRCodeImage($qrtext, $size)
|
||||
|
||||
public function getQRCodeImage(string $qrtext, int $size): string
|
||||
{
|
||||
return $this->getContent($this->getUrl($qrtext, $size));
|
||||
}
|
||||
|
||||
private function decodeColor($value)
|
||||
{
|
||||
return vsprintf('%d-%d-%d', sscanf($value, "%02x%02x%02x"));
|
||||
}
|
||||
|
||||
public function getUrl($qrtext, $size)
|
||||
|
||||
public function getUrl(string $qrtext, int $size): string
|
||||
{
|
||||
return 'https://api.qrserver.com/v1/create-qr-code/'
|
||||
. '?size=' . $size . 'x' . $size
|
||||
@@ -68,4 +48,9 @@ class QRServerProvider extends BaseHTTPQRCodeProvider
|
||||
. '&format=' . strtolower($this->format)
|
||||
. '&data=' . rawurlencode($qrtext);
|
||||
}
|
||||
}
|
||||
|
||||
private function decodeColor(string $value): string
|
||||
{
|
||||
return vsprintf('%d-%d-%d', sscanf($value, '%02x%02x%02x'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth\Providers\Qr;
|
||||
|
||||
// http://qrickit.com/qrickit_apps/qrickit_api.php
|
||||
class QRicketProvider extends BaseHTTPQRCodeProvider
|
||||
/**
|
||||
* Use http://qrickit.com/qrickit_apps/qrickit_api.php to provide a QR code
|
||||
*/
|
||||
class QRicketProvider extends BaseHTTPQRCodeProvider
|
||||
{
|
||||
public $errorcorrectionlevel;
|
||||
public $margin;
|
||||
public $qzone;
|
||||
public $bgcolor;
|
||||
public $color;
|
||||
public $format;
|
||||
|
||||
function __construct($errorcorrectionlevel = 'L', $bgcolor = 'ffffff', $color = '000000', $format = 'p')
|
||||
public function __construct(public string $errorcorrectionlevel = 'L', public string $bgcolor = 'ffffff', public string $color = '000000', public string $format = 'p')
|
||||
{
|
||||
$this->verifyssl = false;
|
||||
|
||||
$this->errorcorrectionlevel = $errorcorrectionlevel;
|
||||
$this->bgcolor = $bgcolor;
|
||||
$this->color = $color;
|
||||
$this->format = $format;
|
||||
}
|
||||
|
||||
public function getMimeType()
|
||||
|
||||
public function getMimeType(): string
|
||||
{
|
||||
switch (strtolower($this->format))
|
||||
{
|
||||
case 'p':
|
||||
switch (strtolower($this->format)) {
|
||||
case 'p':
|
||||
return 'image/png';
|
||||
case 'g':
|
||||
case 'g':
|
||||
return 'image/gif';
|
||||
case 'j':
|
||||
case 'j':
|
||||
return 'image/jpeg';
|
||||
}
|
||||
throw new QRException(sprintf('Unknown MIME-type: %s', $this->format));
|
||||
}
|
||||
|
||||
public function getQRCodeImage($qrtext, $size)
|
||||
|
||||
public function getQRCodeImage(string $qrtext, int $size): string
|
||||
{
|
||||
return $this->getContent($this->getUrl($qrtext, $size));
|
||||
}
|
||||
|
||||
public function getUrl($qrtext, $size)
|
||||
|
||||
public function getUrl(string $qrtext, int $size): string
|
||||
{
|
||||
return 'http://qrickit.com/api/qr'
|
||||
. '?qrsize=' . $size
|
||||
. '?qrsize=' . (string) $size
|
||||
. '&e=' . strtolower($this->errorcorrectionlevel)
|
||||
. '&bgdcolor=' . $this->bgcolor
|
||||
. '&fgdcolor=' . $this->color
|
||||
. '&t=' . strtolower($this->format)
|
||||
. '&d=' . rawurlencode($qrtext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth\Providers\Rng;
|
||||
|
||||
class CSRNGProvider implements IRNGProvider
|
||||
{
|
||||
public function getRandomBytes($bytecount) {
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getRandomBytes(int $bytecount): string
|
||||
{
|
||||
return random_bytes($bytecount); // PHP7+
|
||||
}
|
||||
|
||||
public function isCryptographicallySecure() {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isCryptographicallySecure(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth\Providers\Rng;
|
||||
|
||||
use function in_array;
|
||||
|
||||
class HashRNGProvider implements IRNGProvider
|
||||
{
|
||||
private $algorithm;
|
||||
|
||||
function __construct($algorithm = 'sha256' ) {
|
||||
public function __construct(private string $algorithm = 'sha256')
|
||||
{
|
||||
$algos = array_values(hash_algos());
|
||||
if (!in_array($algorithm, $algos, true))
|
||||
if (!in_array($this->algorithm, $algos, true)) {
|
||||
throw new RNGException('Unsupported algorithm specified');
|
||||
$this->algorithm = $algorithm;
|
||||
}
|
||||
}
|
||||
|
||||
public function getRandomBytes($bytecount) {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getRandomBytes(int $bytecount): string
|
||||
{
|
||||
$result = '';
|
||||
$hash = mt_rand();
|
||||
for ($i = 0; $i < $bytecount; $i++) {
|
||||
$hash = hash($this->algorithm, $hash.mt_rand(), true);
|
||||
$result .= $hash[mt_rand(0, sizeof($hash))];
|
||||
$hash = hash($this->algorithm, $hash . mt_rand(), true);
|
||||
$result .= $hash[mt_rand(0, strlen($hash) - 1)];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function isCryptographicallySecure() {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isCryptographicallySecure(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth\Providers\Rng;
|
||||
|
||||
interface IRNGProvider
|
||||
{
|
||||
public function getRandomBytes($bytecount);
|
||||
public function isCryptographicallySecure();
|
||||
}
|
||||
public function getRandomBytes(int $bytecount): string;
|
||||
|
||||
public function isCryptographicallySecure(): bool;
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace RobThree\Auth\Providers\Rng;
|
||||
|
||||
class MCryptRNGProvider implements IRNGProvider
|
||||
{
|
||||
private $source;
|
||||
|
||||
function __construct($source = MCRYPT_DEV_URANDOM) {
|
||||
$this->source = $source;
|
||||
}
|
||||
|
||||
public function getRandomBytes($bytecount) {
|
||||
$result = mcrypt_create_iv($bytecount, $this->source);
|
||||
if ($result === false)
|
||||
throw new RNGException('mcrypt_create_iv returned an invalid value');
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function isCryptographicallySecure() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth\Providers\Rng;
|
||||
|
||||
class OpenSSLRNGProvider implements IRNGProvider
|
||||
{
|
||||
private $requirestrong;
|
||||
|
||||
function __construct($requirestrong = true) {
|
||||
$this->requirestrong = $requirestrong;
|
||||
public function __construct(private bool $requirestrong = true)
|
||||
{
|
||||
}
|
||||
|
||||
public function getRandomBytes($bytecount) {
|
||||
$result = openssl_random_pseudo_bytes($bytecount, $crypto_strong);
|
||||
if ($this->requirestrong && ($crypto_strong === false))
|
||||
throw new RNGException('openssl_random_pseudo_bytes returned non-cryptographically strong value');
|
||||
if ($result === false)
|
||||
throw new RNGException('openssl_random_pseudo_bytes returned an invalid value');
|
||||
return $result;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getRandomBytes(int $bytecount): string
|
||||
{
|
||||
// will throw an Exception on failure
|
||||
return openssl_random_pseudo_bytes($bytecount, $crypto_strong);
|
||||
}
|
||||
|
||||
public function isCryptographicallySecure() {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isCryptographicallySecure(): bool
|
||||
{
|
||||
return $this->requirestrong;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth\Providers\Rng;
|
||||
|
||||
class RNGException extends \Exception
|
||||
use RobThree\Auth\TwoFactorAuthException;
|
||||
|
||||
class RNGException extends TwoFactorAuthException
|
||||
{
|
||||
function __construct($message = "", $code = 0, $exception = null)
|
||||
{
|
||||
parent::__construct($message, $code, $exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth\Providers\Time;
|
||||
|
||||
use DateTime;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Takes the time from any webserver by doing a HEAD request on the specified URL and extracting the 'Date:' header
|
||||
*/
|
||||
class HttpTimeProvider implements ITimeProvider
|
||||
{
|
||||
/** @var array<string, mixed> */
|
||||
public array $options;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
public function __construct(
|
||||
public string $url = 'https://google.com',
|
||||
public string $expectedtimeformat = 'D, d M Y H:i:s O+',
|
||||
array $options = null,
|
||||
) {
|
||||
$this->url = $url;
|
||||
$this->expectedtimeformat = $expectedtimeformat;
|
||||
if ($options === null) {
|
||||
$options = array(
|
||||
'http' => array(
|
||||
'method' => 'HEAD',
|
||||
'follow_location' => false,
|
||||
'ignore_errors' => true,
|
||||
'max_redirects' => 0,
|
||||
'request_fulluri' => true,
|
||||
'header' => array(
|
||||
'Connection: close',
|
||||
'User-agent: TwoFactorAuth HttpTimeProvider (https://github.com/RobThree/TwoFactorAuth)',
|
||||
'Cache-Control: no-cache',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
$this->options = $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getTime()
|
||||
{
|
||||
try {
|
||||
$context = stream_context_create($this->options);
|
||||
$fd = fopen($this->url, 'rb', false, $context);
|
||||
$headers = stream_get_meta_data($fd);
|
||||
fclose($fd);
|
||||
|
||||
foreach ($headers['wrapper_data'] as $h) {
|
||||
if (strcasecmp(substr($h, 0, 5), 'Date:') === 0) {
|
||||
return DateTime::createFromFormat($this->expectedtimeformat, trim(substr($h, 5)))->getTimestamp();
|
||||
}
|
||||
}
|
||||
throw new Exception('Invalid or no "Date:" header found');
|
||||
} catch (Exception $ex) {
|
||||
throw new TimeException(sprintf('Unable to retrieve time from %s (%s)', $this->url, $ex->getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth\Providers\Time;
|
||||
|
||||
interface ITimeProvider
|
||||
{
|
||||
/**
|
||||
* @return int the current timestamp according to this provider
|
||||
*/
|
||||
public function getTime();
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth\Providers\Time;
|
||||
|
||||
class LocalMachineTimeProvider implements ITimeProvider
|
||||
{
|
||||
public function getTime()
|
||||
{
|
||||
return time();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth\Providers\Time;
|
||||
|
||||
use Exception;
|
||||
|
||||
use function socket_create;
|
||||
|
||||
/**
|
||||
* Takes the time from any NTP server
|
||||
*/
|
||||
class NTPTimeProvider implements ITimeProvider
|
||||
{
|
||||
public function __construct(public string $host = 'time.google.com', public int $port = 123, public int $timeout = 1)
|
||||
{
|
||||
if ($this->port <= 0 || $this->port > 65535) {
|
||||
throw new TimeException('Port must be 0 < port < 65535');
|
||||
}
|
||||
|
||||
if ($this->timeout < 0) {
|
||||
throw new TimeException('Timeout must be >= 0');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getTime()
|
||||
{
|
||||
try {
|
||||
// Create a socket and connect to NTP server
|
||||
$sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
|
||||
socket_set_option($sock, SOL_SOCKET, SO_RCVTIMEO, array('sec' => $this->timeout, 'usec' => 0));
|
||||
socket_connect($sock, $this->host, $this->port);
|
||||
|
||||
// Send request
|
||||
$msg = "\010" . str_repeat("\0", 47);
|
||||
socket_send($sock, $msg, strlen($msg), 0);
|
||||
|
||||
// Receive response and close socket
|
||||
if (socket_recv($sock, $recv, 48, MSG_WAITALL) === false) {
|
||||
throw new Exception(socket_strerror(socket_last_error($sock)));
|
||||
}
|
||||
socket_close($sock);
|
||||
|
||||
// Interpret response
|
||||
$data = unpack('N12', $recv);
|
||||
$timestamp = (int) sprintf('%u', $data[9]);
|
||||
|
||||
// NTP is number of seconds since 0000 UT on 1 January 1900 Unix time is seconds since 0000 UT on 1 January 1970
|
||||
return $timestamp - 2208988800;
|
||||
} catch (Exception $ex) {
|
||||
throw new TimeException(sprintf('Unable to retrieve time from %s (%s)', $this->host, $ex->getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth\Providers\Time;
|
||||
|
||||
use RobThree\Auth\TwoFactorAuthException;
|
||||
|
||||
class TimeException extends TwoFactorAuthException
|
||||
{
|
||||
}
|
||||
+199
-129
@@ -1,194 +1,264 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth;
|
||||
|
||||
use RobThree\Auth\Providers\Qr\IQRCodeProvider;
|
||||
use RobThree\Auth\Providers\Qr\QRServerProvider;
|
||||
use RobThree\Auth\Providers\Rng\CSRNGProvider;
|
||||
use RobThree\Auth\Providers\Rng\HashRNGProvider;
|
||||
use RobThree\Auth\Providers\Rng\IRNGProvider;
|
||||
use RobThree\Auth\Providers\Rng\OpenSSLRNGProvider;
|
||||
use RobThree\Auth\Providers\Time\HttpTimeProvider;
|
||||
use RobThree\Auth\Providers\Time\ITimeProvider;
|
||||
use RobThree\Auth\Providers\Time\LocalMachineTimeProvider;
|
||||
use RobThree\Auth\Providers\Time\NTPTimeProvider;
|
||||
|
||||
// Based on / inspired by: https://github.com/PHPGangsta/GoogleAuthenticator
|
||||
// Algorithms, digits, period etc. explained: https://github.com/google/google-authenticator/wiki/Key-Uri-Format
|
||||
class TwoFactorAuth
|
||||
class TwoFactorAuth
|
||||
{
|
||||
private $algorithm;
|
||||
private $period;
|
||||
private $digits;
|
||||
private $issuer;
|
||||
private $qrcodeprovider;
|
||||
private $rngprovider;
|
||||
private static $_base32dict = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567=';
|
||||
private static $_base32;
|
||||
private static $_base32lookup = array();
|
||||
private static $_supportedalgos = array('sha1', 'sha256', 'sha512', 'md5');
|
||||
|
||||
function __construct($issuer = null, $digits = 6, $period = 30, $algorithm = 'sha1', $qrcodeprovider = null, $rngprovider = null)
|
||||
{
|
||||
$this->issuer = $issuer;
|
||||
private static string $_base32dict = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567=';
|
||||
|
||||
if (!is_int($digits) || $digits <= 0)
|
||||
throw new TwoFactorAuthException('Digits must be int > 0');
|
||||
$this->digits = $digits;
|
||||
|
||||
if (!is_int($period) || $period <= 0)
|
||||
throw new TwoFactorAuthException('Period must be int > 0');
|
||||
$this->period = $period;
|
||||
|
||||
$algorithm = strtolower(trim($algorithm));
|
||||
if (!in_array($algorithm, self::$_supportedalgos))
|
||||
throw new TwoFactorAuthException('Unsupported algorithm: ' . $algorithm);
|
||||
$this->algorithm = $algorithm;
|
||||
|
||||
// Set default QR Code provider if none was specified
|
||||
if ($qrcodeprovider==null)
|
||||
$qrcodeprovider = new Providers\Qr\GoogleQRCodeProvider();
|
||||
|
||||
if (!($qrcodeprovider instanceof Providers\Qr\IQRCodeProvider))
|
||||
throw new TwoFactorAuthException('QRCodeProvider must implement IQRCodeProvider');
|
||||
|
||||
$this->qrcodeprovider = $qrcodeprovider;
|
||||
|
||||
// Try to find best available RNG provider if none was specified
|
||||
if ($rngprovider==null) {
|
||||
if (function_exists('random_bytes')) {
|
||||
$rngprovider = new Providers\Rng\CSRNGProvider();
|
||||
} elseif (function_exists('mcrypt_create_iv')) {
|
||||
$rngprovider = new Providers\Rng\MCryptRNGProvider();
|
||||
} elseif (function_exists('openssl_random_pseudo_bytes')) {
|
||||
$rngprovider = new Providers\Rng\OpenSSLRNGProvider();
|
||||
} elseif (function_exists('hash')) {
|
||||
$rngprovider = new Providers\Rng\HashRNGProvider();
|
||||
} else {
|
||||
throw new TwoFactorAuthException('Unable to find a suited RNGProvider');
|
||||
}
|
||||
/** @var array<string> */
|
||||
private static array $_base32;
|
||||
|
||||
/** @var array<string, int> */
|
||||
private static array $_base32lookup = array();
|
||||
|
||||
public function __construct(
|
||||
private ?string $issuer = null,
|
||||
private int $digits = 6,
|
||||
private int $period = 30,
|
||||
private Algorithm $algorithm = Algorithm::Sha1,
|
||||
private ?IQRCodeProvider $qrcodeprovider = null,
|
||||
private ?IRNGProvider $rngprovider = null,
|
||||
private ?ITimeProvider $timeprovider = null
|
||||
) {
|
||||
if ($this->digits <= 0) {
|
||||
throw new TwoFactorAuthException('Digits must be > 0');
|
||||
}
|
||||
|
||||
if (!($rngprovider instanceof Providers\Rng\IRNGProvider))
|
||||
throw new TwoFactorAuthException('RNGProvider must implement IRNGProvider');
|
||||
|
||||
$this->rngprovider = $rngprovider;
|
||||
|
||||
|
||||
if ($this->period <= 0) {
|
||||
throw new TwoFactorAuthException('Period must be int > 0');
|
||||
}
|
||||
|
||||
self::$_base32 = str_split(self::$_base32dict);
|
||||
self::$_base32lookup = array_flip(self::$_base32);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a new secret
|
||||
*/
|
||||
public function createSecret($bits = 80, $requirecryptosecure = true)
|
||||
public function createSecret(int $bits = 80, bool $requirecryptosecure = true): string
|
||||
{
|
||||
$secret = '';
|
||||
$bytes = ceil($bits / 5); //We use 5 bits of each byte (since we have a 32-character 'alphabet' / BASE32)
|
||||
if ($requirecryptosecure && !$this->rngprovider->isCryptographicallySecure())
|
||||
$bytes = (int) ceil($bits / 5); // We use 5 bits of each byte (since we have a 32-character 'alphabet' / BASE32)
|
||||
$rngprovider = $this->getRngProvider();
|
||||
if ($requirecryptosecure && !$rngprovider->isCryptographicallySecure()) {
|
||||
throw new TwoFactorAuthException('RNG provider is not cryptographically secure');
|
||||
$rnd = $this->rngprovider->getRandomBytes($bytes);
|
||||
for ($i = 0; $i < $bytes; $i++)
|
||||
}
|
||||
$rnd = $rngprovider->getRandomBytes($bytes);
|
||||
for ($i = 0; $i < $bytes; $i++) {
|
||||
$secret .= self::$_base32[ord($rnd[$i]) & 31]; //Mask out left 3 bits for 0-31 values
|
||||
}
|
||||
return $secret;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Calculate the code with given secret and point in time
|
||||
*/
|
||||
public function getCode($secret, $time = null)
|
||||
public function getCode(string $secret, ?int $time = null): string
|
||||
{
|
||||
$secretkey = $this->base32Decode($secret);
|
||||
|
||||
|
||||
$timestamp = "\0\0\0\0" . pack('N*', $this->getTimeSlice($this->getTime($time))); // Pack time into binary string
|
||||
$hashhmac = hash_hmac($this->algorithm, $timestamp, $secretkey, true); // Hash it with users secret key
|
||||
$hashhmac = hash_hmac($this->algorithm->value, $timestamp, $secretkey, true); // Hash it with users secret key
|
||||
$hashpart = substr($hashhmac, ord(substr($hashhmac, -1)) & 0x0F, 4); // Use last nibble of result as index/offset and grab 4 bytes of the result
|
||||
$value = unpack('N', $hashpart); // Unpack binary value
|
||||
$value = $value[1] & 0x7FFFFFFF; // Drop MSB, keep only 31 bits
|
||||
|
||||
return str_pad($value % pow(10, $this->digits), $this->digits, '0', STR_PAD_LEFT);
|
||||
|
||||
return str_pad((string) ($value % 10** $this->digits), $this->digits, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if the code is correct. This will accept codes starting from ($discrepancy * $period) sec ago to ($discrepancy * period) sec from now
|
||||
*/
|
||||
public function verifyCode($secret, $code, $discrepancy = 1, $time = null)
|
||||
public function verifyCode(string $secret, string $code, int $discrepancy = 1, ?int $time = null, ?int &$timeslice = 0): bool
|
||||
{
|
||||
$result = false;
|
||||
$timetamp = $this->getTime($time);
|
||||
|
||||
// To keep safe from timing-attachs we iterate *all* possible codes even though we already may have verified a code is correct
|
||||
for ($i = -$discrepancy; $i <= $discrepancy; $i++)
|
||||
$result |= $this->codeEquals($this->getCode($secret, $timetamp + ($i * $this->period)), $code);
|
||||
|
||||
return (bool)$result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Timing-attack safe comparison of 2 codes (see http://blog.ircmaxell.com/2014/11/its-all-about-time.html)
|
||||
*/
|
||||
private function codeEquals($safe, $user) {
|
||||
if (function_exists('hash_equals')) {
|
||||
return hash_equals($safe, $user);
|
||||
} else {
|
||||
// In general, it's not possible to prevent length leaks. So it's OK to leak the length. The important part is that
|
||||
// we don't leak information about the difference of the two strings.
|
||||
if (strlen($safe)===strlen($user)) {
|
||||
$result = 0;
|
||||
for ($i = 0; $i < strlen($safe); $i++)
|
||||
$result |= (ord($safe[$i]) ^ ord($user[$i]));
|
||||
return $result === 0;
|
||||
}
|
||||
$timestamp = $this->getTime($time);
|
||||
|
||||
$timeslice = 0;
|
||||
|
||||
// To keep safe from timing-attacks we iterate *all* possible codes even though we already may have
|
||||
// verified a code is correct. We use the timeslice variable to hold either 0 (no match) or the timeslice
|
||||
// of the match. Each iteration we either set the timeslice variable to the timeslice of the match
|
||||
// or set the value to itself. This is an effort to maintain constant execution time for the code.
|
||||
for ($i = -$discrepancy; $i <= $discrepancy; $i++) {
|
||||
$ts = $timestamp + ($i * $this->period);
|
||||
$slice = $this->getTimeSlice($ts);
|
||||
$timeslice = $this->codeEquals($this->getCode($secret, $ts), $code) ? $slice : $timeslice;
|
||||
}
|
||||
return false;
|
||||
|
||||
return $timeslice > 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get data-uri of QRCode
|
||||
*/
|
||||
public function getQRCodeImageAsDataUri($label, $secret, $size = 200)
|
||||
public function getQRCodeImageAsDataUri(string $label, string $secret, int $size = 200): string
|
||||
{
|
||||
if (!is_int($size) || $size <= 0)
|
||||
throw new TwoFactorAuthException('Size must be int > 0');
|
||||
|
||||
if ($size <= 0) {
|
||||
throw new TwoFactorAuthException('Size must be > 0');
|
||||
}
|
||||
|
||||
$qrcodeprovider = $this->getQrCodeProvider();
|
||||
return 'data:'
|
||||
. $this->qrcodeprovider->getMimeType()
|
||||
. $qrcodeprovider->getMimeType()
|
||||
. ';base64,'
|
||||
. base64_encode($this->qrcodeprovider->getQRCodeImage($this->getQRText($label, $secret), $size));
|
||||
. base64_encode($qrcodeprovider->getQRCodeImage($this->getQRText($label, $secret), $size));
|
||||
}
|
||||
|
||||
private function getTime($time)
|
||||
|
||||
/**
|
||||
* Compare default timeprovider with specified timeproviders and ensure the time is within the specified number of seconds (leniency)
|
||||
* @param array<ITimeProvider> $timeproviders
|
||||
*/
|
||||
public function ensureCorrectTime(?array $timeproviders = null, int $leniency = 5): void
|
||||
{
|
||||
return ($time === null) ? time() : $time;
|
||||
if ($timeproviders === null) {
|
||||
$timeproviders = array(
|
||||
new NTPTimeProvider(),
|
||||
new HttpTimeProvider(),
|
||||
);
|
||||
}
|
||||
|
||||
// Get default time provider
|
||||
$timeprovider = $this->getTimeProvider();
|
||||
|
||||
// Iterate specified time providers
|
||||
foreach ($timeproviders as $t) {
|
||||
if (!($t instanceof ITimeProvider)) {
|
||||
throw new TwoFactorAuthException('Object does not implement ITimeProvider');
|
||||
}
|
||||
|
||||
// Get time from default time provider and compare to specific time provider and throw if time difference is more than specified number of seconds leniency
|
||||
if (abs($timeprovider->getTime() - $t->getTime()) > $leniency) {
|
||||
throw new TwoFactorAuthException(sprintf('Time for timeprovider is off by more than %d seconds when compared to %s', $leniency, get_class($t)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getTimeSlice($time = null, $offset = 0)
|
||||
{
|
||||
return (int)floor($time / $this->period) + ($offset * $this->period);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Builds a string to be encoded in a QR code
|
||||
*/
|
||||
private function getQRText($label, $secret)
|
||||
public function getQRText(string $label, string $secret): string
|
||||
{
|
||||
return 'otpauth://totp/' . rawurlencode($label)
|
||||
. '?secret=' . rawurlencode($secret)
|
||||
. '&issuer=' . rawurlencode($this->issuer)
|
||||
. '&issuer=' . rawurlencode((string)$this->issuer)
|
||||
. '&period=' . intval($this->period)
|
||||
. '&algorithm=' . rawurlencode(strtoupper($this->algorithm))
|
||||
. '&algorithm=' . rawurlencode(strtoupper($this->algorithm->value))
|
||||
. '&digits=' . intval($this->digits);
|
||||
}
|
||||
|
||||
private function base32Decode($value)
|
||||
|
||||
/**
|
||||
* @throws TwoFactorAuthException
|
||||
*/
|
||||
public function getQrCodeProvider(): IQRCodeProvider
|
||||
{
|
||||
if (strlen($value)==0) return '';
|
||||
|
||||
if (preg_match('/[^'.preg_quote(self::$_base32dict).']/', $value) !== 0)
|
||||
// Set default QR Code provider if none was specified
|
||||
if (null === $this->qrcodeprovider) {
|
||||
return $this->qrcodeprovider = new QRServerProvider();
|
||||
}
|
||||
return $this->qrcodeprovider;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TwoFactorAuthException
|
||||
*/
|
||||
public function getRngProvider(): IRNGProvider
|
||||
{
|
||||
if (null !== $this->rngprovider) {
|
||||
return $this->rngprovider;
|
||||
}
|
||||
if (function_exists('random_bytes')) {
|
||||
return $this->rngprovider = new CSRNGProvider();
|
||||
}
|
||||
if (function_exists('openssl_random_pseudo_bytes')) {
|
||||
return $this->rngprovider = new OpenSSLRNGProvider();
|
||||
}
|
||||
if (function_exists('hash')) {
|
||||
return $this->rngprovider = new HashRNGProvider();
|
||||
}
|
||||
throw new TwoFactorAuthException('Unable to find a suited RNGProvider');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TwoFactorAuthException
|
||||
*/
|
||||
public function getTimeProvider(): ITimeProvider
|
||||
{
|
||||
// Set default time provider if none was specified
|
||||
if (null === $this->timeprovider) {
|
||||
return $this->timeprovider = new LocalMachineTimeProvider();
|
||||
}
|
||||
return $this->timeprovider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Timing-attack safe comparison of 2 codes (see http://blog.ircmaxell.com/2014/11/its-all-about-time.html)
|
||||
*/
|
||||
private function codeEquals(string $safe, string $user): bool
|
||||
{
|
||||
if (function_exists('hash_equals')) {
|
||||
return hash_equals($safe, $user);
|
||||
}
|
||||
// In general, it's not possible to prevent length leaks. So it's OK to leak the length. The important part is that
|
||||
// we don't leak information about the difference of the two strings.
|
||||
if (strlen($safe) === strlen($user)) {
|
||||
$result = 0;
|
||||
for ($i = 0; $i < strlen($safe); $i++) {
|
||||
$result |= (ord($safe[$i]) ^ ord($user[$i]));
|
||||
}
|
||||
return $result === 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function getTime(?int $time = null): int
|
||||
{
|
||||
return ($time === null) ? $this->getTimeProvider()->getTime() : $time;
|
||||
}
|
||||
|
||||
private function getTimeSlice(?int $time = null, int $offset = 0): int
|
||||
{
|
||||
return (int) floor($time / $this->period) + ($offset * $this->period);
|
||||
}
|
||||
|
||||
private function base32Decode(string $value): string
|
||||
{
|
||||
if (strlen($value) == 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (preg_match('/[^' . preg_quote(self::$_base32dict) . ']/', $value) !== 0) {
|
||||
throw new TwoFactorAuthException('Invalid base32 string');
|
||||
|
||||
}
|
||||
|
||||
$buffer = '';
|
||||
foreach (str_split($value) as $char)
|
||||
{
|
||||
if ($char !== '=')
|
||||
$buffer .= str_pad(decbin(self::$_base32lookup[$char]), 5, 0, STR_PAD_LEFT);
|
||||
foreach (str_split($value) as $char) {
|
||||
if ($char !== '=') {
|
||||
$buffer .= str_pad(decbin(self::$_base32lookup[$char]), 5, '0', STR_PAD_LEFT);
|
||||
}
|
||||
}
|
||||
$length = strlen($buffer);
|
||||
$blocks = trim(chunk_split(substr($buffer, 0, $length - ($length % 8)), 8, ' '));
|
||||
|
||||
$output = '';
|
||||
foreach (explode(' ', $blocks) as $block)
|
||||
$output .= chr(bindec(str_pad($block, 8, 0, STR_PAD_RIGHT)));
|
||||
|
||||
$output = '';
|
||||
foreach (explode(' ', $blocks) as $block) {
|
||||
$output .= chr(bindec(str_pad($block, 8, '0', STR_PAD_RIGHT)));
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth;
|
||||
|
||||
class TwoFactorAuthException extends \Exception
|
||||
use Exception;
|
||||
|
||||
class TwoFactorAuthException extends Exception
|
||||
{
|
||||
function __construct($message = "", $code = 0, $exception = null)
|
||||
{
|
||||
parent::__construct($message, $code, $exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
parameters:
|
||||
level: 6
|
||||
|
||||
excludePaths:
|
||||
- %currentWorkingDirectory%/lib/Providers/Qr/BaconQrCodeProvider.php
|
||||
- %currentWorkingDirectory%/lib/Providers/Qr/EndroidQrCodeProvider.php
|
||||
- %currentWorkingDirectory%/lib/Providers/Qr/EndroidQrCodeWithLogoProvider.php
|
||||
|
||||
paths:
|
||||
- %currentWorkingDirectory%/lib
|
||||
- %currentWorkingDirectory%/tests
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit backupGlobals="false"
|
||||
backupStaticAttributes="false"
|
||||
colors="true"
|
||||
convertDeprecationsToExceptions="true"
|
||||
convertErrorsToExceptions="true"
|
||||
convertNoticesToExceptions="true"
|
||||
convertWarningsToExceptions="true"
|
||||
processIsolation="false"
|
||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
stopOnFailure="false">
|
||||
<testsuites>
|
||||
<testsuite name="Unit">
|
||||
<directory suffix="Test.php">./tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<coverage processUncoveredFiles="true">
|
||||
<include>
|
||||
<directory suffix=".php">./lib</directory>
|
||||
</include>
|
||||
<report>
|
||||
<html outputDirectory="build/coverage"/>
|
||||
<text outputFile="php://stdout"/>
|
||||
</report>
|
||||
</coverage>
|
||||
</phpunit>
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Providers\Qr;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use RobThree\Auth\Algorithm;
|
||||
use RobThree\Auth\Providers\Qr\HandlesDataUri;
|
||||
use RobThree\Auth\TwoFactorAuth;
|
||||
use RobThree\Auth\TwoFactorAuthException;
|
||||
|
||||
class IQRCodeProviderTest extends TestCase
|
||||
{
|
||||
use HandlesDataUri;
|
||||
|
||||
public function testTotpUriIsCorrect(): void
|
||||
{
|
||||
$qr = new TestQrProvider();
|
||||
|
||||
$tfa = new TwoFactorAuth('Test&Issuer', 6, 30, Algorithm::Sha1, $qr);
|
||||
$data = $this->DecodeDataUri($tfa->getQRCodeImageAsDataUri('Test&Label', 'VMR466AB62ZBOKHE'));
|
||||
$this->assertEquals('test/test', $data['mimetype']);
|
||||
$this->assertEquals('base64', $data['encoding']);
|
||||
$this->assertEquals('otpauth://totp/Test%26Label?secret=VMR466AB62ZBOKHE&issuer=Test%26Issuer&period=30&algorithm=SHA1&digits=6@200', $data['data']);
|
||||
}
|
||||
|
||||
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);
|
||||
$data = $this->DecodeDataUri($tfa->getQRCodeImageAsDataUri('Test&Label', 'VMR466AB62ZBOKHE'));
|
||||
$this->assertEquals('test/test', $data['mimetype']);
|
||||
$this->assertEquals('base64', $data['encoding']);
|
||||
$this->assertEquals('otpauth://totp/Test%26Label?secret=VMR466AB62ZBOKHE&issuer=&period=30&algorithm=SHA1&digits=6@200', $data['data']);
|
||||
}
|
||||
|
||||
public function testGetQRCodeImageAsDataUriThrowsOnInvalidSize(): void
|
||||
{
|
||||
$qr = new TestQrProvider();
|
||||
|
||||
$tfa = new TwoFactorAuth('Test', 6, 30, Algorithm::Sha1, $qr);
|
||||
|
||||
$this->expectException(TwoFactorAuthException::class);
|
||||
|
||||
$tfa->getQRCodeImageAsDataUri('Test', 'VMR466AB62ZBOKHE', 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Providers\Qr;
|
||||
|
||||
use RobThree\Auth\Providers\Qr\IQRCodeProvider;
|
||||
|
||||
class TestQrProvider implements IQRCodeProvider
|
||||
{
|
||||
public function getQRCodeImage(string $qrtext, int $size): string
|
||||
{
|
||||
return $qrtext . '@' . $size;
|
||||
}
|
||||
|
||||
public function getMimeType(): string
|
||||
{
|
||||
return 'test/test';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Providers\Rng;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use RobThree\Auth\Providers\Rng\CSRNGProvider;
|
||||
|
||||
class CSRNGProviderTest extends TestCase
|
||||
{
|
||||
use NeedsRngLengths;
|
||||
|
||||
/**
|
||||
* @requires function random_bytes
|
||||
*/
|
||||
public function testCSRNGProvidersReturnExpectedNumberOfBytes(): void
|
||||
{
|
||||
if (function_exists('random_bytes')) {
|
||||
$rng = new CSRNGProvider();
|
||||
foreach ($this->rngTestLengths as $l) {
|
||||
$this->assertEquals($l, strlen($rng->getRandomBytes($l)));
|
||||
}
|
||||
$this->assertTrue($rng->isCryptographicallySecure());
|
||||
} else {
|
||||
$this->expectNotToPerformAssertions();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Providers\Rng;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use RobThree\Auth\Providers\Rng\HashRNGProvider;
|
||||
|
||||
class HashRNGProviderTest extends TestCase
|
||||
{
|
||||
use NeedsRngLengths;
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testHashRNGProvidersReturnExpectedNumberOfBytes()
|
||||
{
|
||||
$rng = new HashRNGProvider();
|
||||
foreach ($this->rngTestLengths as $l) {
|
||||
$this->assertEquals($l, strlen($rng->getRandomBytes($l)));
|
||||
}
|
||||
|
||||
$this->assertFalse($rng->isCryptographicallySecure());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Providers\Rng;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use RobThree\Auth\Algorithm;
|
||||
use RobThree\Auth\TwoFactorAuth;
|
||||
use RobThree\Auth\TwoFactorAuthException;
|
||||
|
||||
class IRNGProviderTest extends TestCase
|
||||
{
|
||||
public function testCreateSecretThrowsOnInsecureRNGProvider(): void
|
||||
{
|
||||
$rng = new TestRNGProvider();
|
||||
|
||||
$tfa = new TwoFactorAuth('Test', 6, 30, Algorithm::Sha1, null, $rng);
|
||||
|
||||
$this->expectException(TwoFactorAuthException::class);
|
||||
$tfa->createSecret();
|
||||
}
|
||||
|
||||
public function testCreateSecretOverrideSecureDoesNotThrowOnInsecureRNG(): void
|
||||
{
|
||||
$rng = new TestRNGProvider();
|
||||
|
||||
$tfa = new TwoFactorAuth('Test', 6, 30, Algorithm::Sha1, null, $rng);
|
||||
$this->assertEquals('ABCDEFGHIJKLMNOP', $tfa->createSecret(80, false));
|
||||
}
|
||||
|
||||
public function testCreateSecretDoesNotThrowOnSecureRNGProvider(): void
|
||||
{
|
||||
$rng = new TestRNGProvider(true);
|
||||
|
||||
$tfa = new TwoFactorAuth('Test', 6, 30, Algorithm::Sha1, null, $rng);
|
||||
$this->assertEquals('ABCDEFGHIJKLMNOP', $tfa->createSecret());
|
||||
}
|
||||
|
||||
public function testCreateSecretGeneratesDesiredAmountOfEntropy(): void
|
||||
{
|
||||
$rng = new TestRNGProvider(true);
|
||||
|
||||
$tfa = new TwoFactorAuth('Test', 6, 30, Algorithm::Sha1, null, $rng);
|
||||
$this->assertEquals('A', $tfa->createSecret(5));
|
||||
$this->assertEquals('AB', $tfa->createSecret(6));
|
||||
$this->assertEquals('ABCDEFGHIJKLMNOPQRSTUVWXYZ', $tfa->createSecret(128));
|
||||
$this->assertEquals('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', $tfa->createSecret(160));
|
||||
$this->assertEquals('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', $tfa->createSecret(320));
|
||||
$this->assertEquals('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUVWXYZ234567A', $tfa->createSecret(321));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Providers\Rng;
|
||||
|
||||
trait NeedsRngLengths
|
||||
{
|
||||
/** @var array<int> */
|
||||
protected $rngTestLengths = array(1, 16, 32, 256);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Providers\Rng;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use RobThree\Auth\Providers\Rng\OpenSSLRNGProvider;
|
||||
|
||||
class OpenSSLRNGProviderTest extends TestCase
|
||||
{
|
||||
use NeedsRngLengths;
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testStrongOpenSSLRNGProvidersReturnExpectedNumberOfBytes()
|
||||
{
|
||||
$rng = new OpenSSLRNGProvider(true);
|
||||
foreach ($this->rngTestLengths as $l) {
|
||||
$this->assertEquals($l, strlen($rng->getRandomBytes($l)));
|
||||
}
|
||||
|
||||
$this->assertTrue($rng->isCryptographicallySecure());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testNonStrongOpenSSLRNGProvidersReturnExpectedNumberOfBytes()
|
||||
{
|
||||
$rng = new OpenSSLRNGProvider(false);
|
||||
foreach ($this->rngTestLengths as $l) {
|
||||
$this->assertEquals($l, strlen($rng->getRandomBytes($l)));
|
||||
}
|
||||
|
||||
$this->assertFalse($rng->isCryptographicallySecure());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Providers\Rng;
|
||||
|
||||
use RobThree\Auth\Providers\Rng\IRNGProvider;
|
||||
|
||||
class TestRNGProvider implements IRNGProvider
|
||||
{
|
||||
public function __construct(private bool $isSecure = false)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getRandomBytes(int $bytecount): string
|
||||
{
|
||||
$result = '';
|
||||
|
||||
for ($i = 0; $i < $bytecount; $i++) {
|
||||
$result .= chr($i);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isCryptographicallySecure(): bool
|
||||
{
|
||||
return $this->isSecure;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Providers\Time;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use RobThree\Auth\Algorithm;
|
||||
use RobThree\Auth\TwoFactorAuth;
|
||||
use RobThree\Auth\TwoFactorAuthException;
|
||||
|
||||
class ITimeProviderTest extends TestCase
|
||||
{
|
||||
public function testEnsureCorrectTimeDoesNotThrowForCorrectTime(): void
|
||||
{
|
||||
$this->expectNotToPerformAssertions();
|
||||
$tpr1 = new TestTimeProvider(123);
|
||||
$tpr2 = new TestTimeProvider(128);
|
||||
|
||||
$tfa = new TwoFactorAuth('Test', 6, 30, Algorithm::Sha1, null, null, $tpr1);
|
||||
$tfa->ensureCorrectTime(array($tpr2)); // 128 - 123 = 5 => within default leniency
|
||||
}
|
||||
|
||||
public function testEnsureCorrectTimeThrowsOnIncorrectTime(): void
|
||||
{
|
||||
$tpr1 = new TestTimeProvider(123);
|
||||
$tpr2 = new TestTimeProvider(124);
|
||||
|
||||
$tfa = new TwoFactorAuth('Test', 6, 30, Algorithm::Sha1, null, null, $tpr1);
|
||||
|
||||
$this->expectException(TwoFactorAuthException::class);
|
||||
|
||||
$tfa->ensureCorrectTime(array($tpr2), 0); // We force a leniency of 0, 124-123 = 1 so this should throw
|
||||
}
|
||||
|
||||
public function testEnsureDefaultTimeProviderReturnsCorrectTime(): void
|
||||
{
|
||||
$this->expectNotToPerformAssertions();
|
||||
$tfa = new TwoFactorAuth('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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Providers\Time;
|
||||
|
||||
use RobThree\Auth\Providers\Time\ITimeProvider;
|
||||
|
||||
class TestTimeProvider implements ITimeProvider
|
||||
{
|
||||
/** @var int */
|
||||
private $time;
|
||||
|
||||
/**
|
||||
* @param int $time
|
||||
*/
|
||||
public function __construct($time)
|
||||
{
|
||||
$this->time = $time;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getTime()
|
||||
{
|
||||
return $this->time;
|
||||
}
|
||||
}
|
||||
+98
-241
@@ -1,165 +1,116 @@
|
||||
<?php
|
||||
require_once 'lib/TwoFactorAuth.php';
|
||||
require_once 'lib/TwoFactorAuthException.php';
|
||||
|
||||
require_once 'lib/Providers/Qr/IQRCodeProvider.php';
|
||||
require_once 'lib/Providers/Qr/BaseHTTPQRCodeProvider.php';
|
||||
require_once 'lib/Providers/Qr/GoogleQRCodeProvider.php';
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once 'lib/Providers/Rng/IRNGProvider.php';
|
||||
require_once 'lib/Providers/Rng/RNGException.php';
|
||||
require_once 'lib/Providers/Rng/CSRNGProvider.php';
|
||||
require_once 'lib/Providers/Rng/MCryptRNGProvider.php';
|
||||
require_once 'lib/Providers/Rng/OpenSSLRNGProvider.php';
|
||||
require_once 'lib/Providers/Rng/HashRNGProvider.php';
|
||||
namespace Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ReflectionMethod;
|
||||
use RobThree\Auth\Algorithm;
|
||||
use RobThree\Auth\TwoFactorAuth;
|
||||
use RobThree\Auth\Providers\Qr\IQRCodeProvider;
|
||||
use RobThree\Auth\Providers\Rng\IRNGProvider;
|
||||
use RobThree\Auth\TwoFactorAuthException;
|
||||
|
||||
|
||||
class TwoFactorAuthTest extends PHPUnit_Framework_TestCase
|
||||
class TwoFactorAuthTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @expectedException \RobThree\Auth\TwoFactorAuthException
|
||||
*/
|
||||
public function testConstructorThrowsOnInvalidDigits() {
|
||||
public function testConstructorThrowsOnInvalidDigits(): void
|
||||
{
|
||||
$this->expectException(TwoFactorAuthException::class);
|
||||
|
||||
new TwoFactorAuth('Test', 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \RobThree\Auth\TwoFactorAuthException
|
||||
*/
|
||||
public function testConstructorThrowsOnInvalidPeriod() {
|
||||
public function testConstructorThrowsOnInvalidPeriod(): void
|
||||
{
|
||||
$this->expectException(TwoFactorAuthException::class);
|
||||
|
||||
new TwoFactorAuth('Test', 6, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \RobThree\Auth\TwoFactorAuthException
|
||||
*/
|
||||
public function testConstructorThrowsOnInvalidAlgorithm() {
|
||||
|
||||
new TwoFactorAuth('Test', 6, 30, 'xxx');
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \RobThree\Auth\TwoFactorAuthException
|
||||
*/
|
||||
public function testConstructorThrowsOnQrProviderNotImplementingInterface() {
|
||||
|
||||
new TwoFactorAuth('Test', 6, 30, 'sha1', new stdClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \RobThree\Auth\TwoFactorAuthException
|
||||
*/
|
||||
public function testConstructorThrowsOnRngProviderNotImplementingInterface() {
|
||||
|
||||
new TwoFactorAuth('Test', 6, 30, 'sha1', null, new stdClass());
|
||||
}
|
||||
|
||||
public function testGetCodeReturnsCorrectResults() {
|
||||
|
||||
public function testGetCodeReturnsCorrectResults(): void
|
||||
{
|
||||
$tfa = new TwoFactorAuth('Test');
|
||||
$this->assertEquals('543160', $tfa->getCode('VMR466AB62ZBOKHE', 1426847216));
|
||||
$this->assertEquals('538532', $tfa->getCode('VMR466AB62ZBOKHE', 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \RobThree\Auth\TwoFactorAuthException
|
||||
*/
|
||||
public function testCreateSecretThrowsOnInsecureRNGProvider() {
|
||||
$rng = new TestRNGProvider();
|
||||
|
||||
$tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, $rng);
|
||||
$tfa->createSecret();
|
||||
public function testEnsureAllTimeProvidersReturnCorrectTime(): void
|
||||
{
|
||||
$tfa = new TwoFactorAuth('Test', 6, 30, Algorithm::Sha1);
|
||||
$tfa->ensureCorrectTime(array(
|
||||
new \RobThree\Auth\Providers\Time\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??
|
||||
new \RobThree\Auth\Providers\Time\HttpTimeProvider(), // Uses google.com by default
|
||||
//new \RobThree\Auth\Providers\Time\HttpTimeProvider('https://github.com'), // github.com will periodically report times that are off by more than 5 sec
|
||||
new \RobThree\Auth\Providers\Time\HttpTimeProvider('https://yahoo.com'),
|
||||
));
|
||||
$this->expectNotToPerformAssertions();
|
||||
}
|
||||
|
||||
public function testCreateSecretOverrideSecureDoesNotThrowOnInsecureRNG() {
|
||||
$rng = new TestRNGProvider();
|
||||
|
||||
$tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, $rng);
|
||||
$this->assertEquals('ABCDEFGHIJKLMNOP', $tfa->createSecret(80, false));
|
||||
}
|
||||
|
||||
public function testCreateSecretDoesNotThrowOnSecureRNGProvider() {
|
||||
$rng = new TestRNGProvider(true);
|
||||
|
||||
$tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, $rng);
|
||||
$this->assertEquals('ABCDEFGHIJKLMNOP', $tfa->createSecret());
|
||||
}
|
||||
|
||||
public function testCreateSecretGeneratesDesiredAmountOfEntropy() {
|
||||
$rng = new TestRNGProvider(true);
|
||||
|
||||
$tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, $rng);
|
||||
$this->assertEquals('A', $tfa->createSecret(5));
|
||||
$this->assertEquals('AB', $tfa->createSecret(6));
|
||||
$this->assertEquals('ABCDEFGHIJKLMNOPQRSTUVWXYZ', $tfa->createSecret(128));
|
||||
$this->assertEquals('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', $tfa->createSecret(160));
|
||||
$this->assertEquals('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', $tfa->createSecret(320));
|
||||
$this->assertEquals('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUVWXYZ234567A', $tfa->createSecret(321));
|
||||
}
|
||||
|
||||
|
||||
public function testVerifyCodeWorksCorrectly() {
|
||||
|
||||
public function testVerifyCodeWorksCorrectly(): void
|
||||
{
|
||||
$tfa = new TwoFactorAuth('Test', 6, 30);
|
||||
$this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847190));
|
||||
$this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 0, 1426847190 + 29)); //Test discrepancy
|
||||
$this->assertEquals(false, $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 0, 1426847190 + 30)); //Test discrepancy
|
||||
$this->assertEquals(false, $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 0, 1426847190 - 1)); //Test discrepancy
|
||||
$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
|
||||
$this->assertFalse($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 0, 1426847190 - 1)); //Test discrepancy
|
||||
|
||||
$this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 + 0)); //Test discrepancy
|
||||
$this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 + 35)); //Test discrepancy
|
||||
$this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 - 35)); //Test discrepancy
|
||||
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 + 0)); //Test discrepancy
|
||||
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 + 35)); //Test discrepancy
|
||||
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 - 35)); //Test discrepancy
|
||||
|
||||
$this->assertEquals(false, $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 + 65)); //Test discrepancy
|
||||
$this->assertEquals(false, $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 - 65)); //Test discrepancy
|
||||
$this->assertFalse($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 + 65)); //Test discrepancy
|
||||
$this->assertFalse($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 - 65)); //Test discrepancy
|
||||
|
||||
$this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 2, 1426847205 + 65)); //Test discrepancy
|
||||
$this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 2, 1426847205 - 65)); //Test discrepancy
|
||||
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 2, 1426847205 + 65)); //Test discrepancy
|
||||
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 2, 1426847205 - 65)); //Test discrepancy
|
||||
}
|
||||
|
||||
public function testTotpUriIsCorrect() {
|
||||
$qr = new TestQrProvider();
|
||||
public function testVerifyCorrectTimeSliceIsReturned(): void
|
||||
{
|
||||
$tfa = new TwoFactorAuth('Test', 6, 30);
|
||||
|
||||
$tfa = new TwoFactorAuth('Test&Issuer', 6, 30, 'sha1', $qr);
|
||||
$data = $this->DecodeDataUri($tfa->getQRCodeImageAsDataUri('Test&Label', 'VMR466AB62ZBOKHE'));
|
||||
$this->assertEquals('test/test', $data['mimetype']);
|
||||
$this->assertEquals('base64', $data['encoding']);
|
||||
$this->assertEquals('otpauth://totp/Test%26Label?secret=VMR466AB62ZBOKHE&issuer=Test%26Issuer&period=30&algorithm=SHA1&digits=6@200', $data['data']);
|
||||
// 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
|
||||
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '534113', 3, 1426847190, $timeslice1));
|
||||
$this->assertEquals(47561570, $timeslice1);
|
||||
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '819652', 3, 1426847190, $timeslice2));
|
||||
$this->assertEquals(47561571, $timeslice2);
|
||||
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '915954', 3, 1426847190, $timeslice3));
|
||||
$this->assertEquals(47561572, $timeslice3);
|
||||
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 3, 1426847190, $timeslice4));
|
||||
$this->assertEquals(47561573, $timeslice4);
|
||||
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '348401', 3, 1426847190, $timeslice5));
|
||||
$this->assertEquals(47561574, $timeslice5);
|
||||
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '648525', 3, 1426847190, $timeslice6));
|
||||
$this->assertEquals(47561575, $timeslice6);
|
||||
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '170645', 3, 1426847190, $timeslice7));
|
||||
$this->assertEquals(47561576, $timeslice7);
|
||||
|
||||
// Incorrect code should return false and a 0 timeslice
|
||||
$this->assertFalse($tfa->verifyCode('VMR466AB62ZBOKHE', '111111', 3, 1426847190, $timeslice8));
|
||||
$this->assertEquals(0, $timeslice8);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \RobThree\Auth\TwoFactorAuthException
|
||||
*/
|
||||
public function testGetQRCodeImageAsDataUriThrowsOnInvalidSize() {
|
||||
$qr = new TestQrProvider();
|
||||
|
||||
$tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', $qr);
|
||||
$tfa->getQRCodeImageAsDataUri('Test', 'VMR466AB62ZBOKHE', 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \RobThree\Auth\TwoFactorAuthException
|
||||
*/
|
||||
public function testGetCodeThrowsOnInvalidBase32String1() {
|
||||
public function testGetCodeThrowsOnInvalidBase32String1(): void
|
||||
{
|
||||
$tfa = new TwoFactorAuth('Test');
|
||||
|
||||
$this->expectException(TwoFactorAuthException::class);
|
||||
|
||||
$tfa->getCode('FOO1BAR8BAZ9'); //1, 8 & 9 are invalid chars
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \RobThree\Auth\TwoFactorAuthException
|
||||
*/
|
||||
public function testGetCodeThrowsOnInvalidBase32String2() {
|
||||
|
||||
public function testGetCodeThrowsOnInvalidBase32String2(): void
|
||||
{
|
||||
$tfa = new TwoFactorAuth('Test');
|
||||
|
||||
$this->expectException(TwoFactorAuthException::class);
|
||||
|
||||
$tfa->getCode('mzxw6==='); //Lowercase
|
||||
}
|
||||
|
||||
public function testKnownBase32DecodeTestVectors() {
|
||||
|
||||
public function testKnownBase32DecodeTestVectors(): void
|
||||
{
|
||||
// We usually don't test internals (e.g. privates) but since we rely heavily on base32 decoding and don't want
|
||||
// to expose this method nor do we want to give people the possibility of implementing / providing their own base32
|
||||
// decoding/decoder (as we do with Rng/QR providers for example) we simply test the private base32Decode() method
|
||||
@@ -167,15 +118,15 @@ class TwoFactorAuthTest extends PHPUnit_Framework_TestCase
|
||||
// be any bugs hiding in there. We **could** 'fool' ourselves by calling the public getCode() method (which uses
|
||||
// base32decode internally) and then make sure getCode's output (in digits) equals expected output since that would
|
||||
// mean the base32Decode() works as expected but that **could** hide some subtle bug(s) in decoding the base32 string.
|
||||
|
||||
|
||||
// "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');
|
||||
|
||||
$method = new ReflectionMethod('RobThree\Auth\TwoFactorAuth', 'base32Decode');
|
||||
|
||||
$method = new ReflectionMethod(TwoFactorAuth::class, 'base32Decode');
|
||||
$method->setAccessible(true);
|
||||
|
||||
|
||||
// Test vectors from: https://tools.ietf.org/html/rfc4648#page-12
|
||||
$this->assertEquals('', $method->invoke($tfa, ''));
|
||||
$this->assertEquals('f', $method->invoke($tfa, 'MY======'));
|
||||
@@ -185,17 +136,18 @@ class TwoFactorAuthTest extends PHPUnit_Framework_TestCase
|
||||
$this->assertEquals('fooba', $method->invoke($tfa, 'MZXW6YTB'));
|
||||
$this->assertEquals('foobar', $method->invoke($tfa, 'MZXW6YTBOI======'));
|
||||
}
|
||||
|
||||
public function testKnownBase32DecodeUnpaddedTestVectors() {
|
||||
|
||||
public function testKnownBase32DecodeUnpaddedTestVectors(): void
|
||||
{
|
||||
// See testKnownBase32DecodeTestVectors() for the rationale behind testing the private base32Decode() method.
|
||||
// This test ensures that strings without the padding-char ('=') are also decoded correctly.
|
||||
// https://tools.ietf.org/html/rfc4648#page-4:
|
||||
// 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');
|
||||
|
||||
$method = new ReflectionMethod('RobThree\Auth\TwoFactorAuth', 'base32Decode');
|
||||
|
||||
$method = new ReflectionMethod(TwoFactorAuth::class, 'base32Decode');
|
||||
$method->setAccessible(true);
|
||||
|
||||
|
||||
// Test vectors from: https://tools.ietf.org/html/rfc4648#page-12
|
||||
$this->assertEquals('', $method->invoke($tfa, ''));
|
||||
$this->assertEquals('f', $method->invoke($tfa, 'MY'));
|
||||
@@ -206,11 +158,11 @@ class TwoFactorAuthTest extends PHPUnit_Framework_TestCase
|
||||
$this->assertEquals('foobar', $method->invoke($tfa, 'MZXW6YTBOI'));
|
||||
}
|
||||
|
||||
|
||||
public function testKnownTestVectors_sha1() {
|
||||
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, 'sha1');
|
||||
$tfa = new TwoFactorAuth('Test', 8, 30, Algorithm::Sha1);
|
||||
$this->assertEquals('94287082', $tfa->getCode($secret, 59));
|
||||
$this->assertEquals('07081804', $tfa->getCode($secret, 1111111109));
|
||||
$this->assertEquals('14050471', $tfa->getCode($secret, 1111111111));
|
||||
@@ -218,11 +170,12 @@ class TwoFactorAuthTest extends PHPUnit_Framework_TestCase
|
||||
$this->assertEquals('69279037', $tfa->getCode($secret, 2000000000));
|
||||
$this->assertEquals('65353130', $tfa->getCode($secret, 20000000000));
|
||||
}
|
||||
|
||||
public function testKnownTestVectors_sha256() {
|
||||
|
||||
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, 'sha256');
|
||||
$tfa = new TwoFactorAuth('Test', 8, 30, Algorithm::Sha256);
|
||||
$this->assertEquals('46119246', $tfa->getCode($secret, 59));
|
||||
$this->assertEquals('68084774', $tfa->getCode($secret, 1111111109));
|
||||
$this->assertEquals('67062674', $tfa->getCode($secret, 1111111111));
|
||||
@@ -230,11 +183,12 @@ class TwoFactorAuthTest extends PHPUnit_Framework_TestCase
|
||||
$this->assertEquals('90698825', $tfa->getCode($secret, 2000000000));
|
||||
$this->assertEquals('77737706', $tfa->getCode($secret, 20000000000));
|
||||
}
|
||||
|
||||
public function testKnownTestVectors_sha512() {
|
||||
|
||||
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, 'sha512');
|
||||
$tfa = new TwoFactorAuth('Test', 8, 30, Algorithm::Sha512);
|
||||
$this->assertEquals('90693936', $tfa->getCode($secret, 59));
|
||||
$this->assertEquals('25091201', $tfa->getCode($secret, 1111111109));
|
||||
$this->assertEquals('99943326', $tfa->getCode($secret, 1111111111));
|
||||
@@ -242,101 +196,4 @@ class TwoFactorAuthTest extends PHPUnit_Framework_TestCase
|
||||
$this->assertEquals('38618901', $tfa->getCode($secret, 2000000000));
|
||||
$this->assertEquals('47863826', $tfa->getCode($secret, 20000000000));
|
||||
}
|
||||
|
||||
/**
|
||||
* @requires function random_bytes
|
||||
*/
|
||||
public function testCSRNGProvidersReturnExpectedNumberOfBytes() {
|
||||
$rng = new \RobThree\Auth\Providers\Rng\CSRNGProvider();
|
||||
foreach ($this->getRngTestLengths() as $l)
|
||||
$this->assertEquals($l, strlen($rng->getRandomBytes($l)));
|
||||
$this->assertEquals(true, $rng->isCryptographicallySecure());
|
||||
}
|
||||
|
||||
/**
|
||||
* @requires function hash_algos
|
||||
* @requires function hash
|
||||
*/
|
||||
public function testHashRNGProvidersReturnExpectedNumberOfBytes() {
|
||||
$rng = new \RobThree\Auth\Providers\Rng\HashRNGProvider();
|
||||
foreach ($this->getRngTestLengths() as $l)
|
||||
$this->assertEquals($l, strlen($rng->getRandomBytes($l)));
|
||||
$this->assertEquals(false, $rng->isCryptographicallySecure());
|
||||
}
|
||||
|
||||
/**
|
||||
* @requires function mcrypt_create_iv
|
||||
*/
|
||||
public function testMCryptRNGProvidersReturnExpectedNumberOfBytes() {
|
||||
$rng = new \RobThree\Auth\Providers\Rng\MCryptRNGProvider();
|
||||
foreach ($this->getRngTestLengths() as $l)
|
||||
$this->assertEquals($l, strlen($rng->getRandomBytes($l)));
|
||||
$this->assertEquals(true, $rng->isCryptographicallySecure());
|
||||
}
|
||||
|
||||
/**
|
||||
* @requires function openssl_random_pseudo_bytes
|
||||
*/
|
||||
public function testStrongOpenSSLRNGProvidersReturnExpectedNumberOfBytes() {
|
||||
$rng = new \RobThree\Auth\Providers\Rng\OpenSSLRNGProvider(true);
|
||||
foreach ($this->getRngTestLengths() as $l)
|
||||
$this->assertEquals($l, strlen($rng->getRandomBytes($l)));
|
||||
$this->assertEquals(true, $rng->isCryptographicallySecure());
|
||||
}
|
||||
|
||||
/**
|
||||
* @requires function openssl_random_pseudo_bytes
|
||||
*/
|
||||
public function testNonStrongOpenSSLRNGProvidersReturnExpectedNumberOfBytes() {
|
||||
$rng = new \RobThree\Auth\Providers\Rng\OpenSSLRNGProvider(false);
|
||||
foreach ($this->getRngTestLengths() as $l)
|
||||
$this->assertEquals($l, strlen($rng->getRandomBytes($l)));
|
||||
$this->assertEquals(false, $rng->isCryptographicallySecure());
|
||||
}
|
||||
|
||||
|
||||
private function getRngTestLengths() {
|
||||
return array(1, 16, 32, 256);
|
||||
}
|
||||
|
||||
private function DecodeDataUri($datauri) {
|
||||
if (preg_match('/data:(?P<mimetype>[\w\.\-\/]+);(?P<encoding>\w+),(?P<data>.*)/', $datauri, $m) === 1) {
|
||||
return array(
|
||||
'mimetype' => $m['mimetype'],
|
||||
'encoding' => $m['encoding'],
|
||||
'data' => base64_decode($m['data'])
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class TestRNGProvider implements IRNGProvider {
|
||||
private $isSecure;
|
||||
|
||||
function __construct($isSecure = false) {
|
||||
$this->isSecure = $isSecure;
|
||||
}
|
||||
|
||||
public function getRandomBytes($bytecount) {
|
||||
$result = '';
|
||||
for ($i=0; $i<$bytecount; $i++)
|
||||
$result.=chr($i);
|
||||
return $result;
|
||||
|
||||
}
|
||||
|
||||
public function isCryptographicallySecure() {
|
||||
return $this->isSecure;
|
||||
}
|
||||
}
|
||||
|
||||
class TestQrProvider implements IQRCodeProvider {
|
||||
public function getQRCodeImage($qrtext, $size) {
|
||||
return $qrtext . '@' . $size;
|
||||
}
|
||||
|
||||
public function getMimeType() {
|
||||
return 'test/test';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace TestsDependency;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use RobThree\Auth\Algorithm;
|
||||
use RobThree\Auth\Providers\Qr\BaconQrCodeProvider;
|
||||
use RobThree\Auth\Providers\Qr\HandlesDataUri;
|
||||
use RobThree\Auth\TwoFactorAuth;
|
||||
use RuntimeException;
|
||||
|
||||
class BaconQRCodeTest extends TestCase
|
||||
{
|
||||
use HandlesDataUri;
|
||||
|
||||
public function testDependency(): void
|
||||
{
|
||||
$qr = new BaconQrCodeProvider(1, '#000', '#FFF', 'svg');
|
||||
|
||||
$tfa = new TwoFactorAuth('Test&Issuer', 6, 30, Algorithm::Sha1, $qr);
|
||||
|
||||
$data = $this->DecodeDataUri($tfa->getQRCodeImageAsDataUri('Test&Label', 'VMR466AB62ZBOKHE'));
|
||||
$this->assertEquals('image/svg+xml', $data['mimetype']);
|
||||
}
|
||||
|
||||
public function testBadTextColour(): void
|
||||
{
|
||||
$this->expectException(RuntimeException::class);
|
||||
|
||||
new BaconQrCodeProvider(1, 'not-a-colour', '#FFF');
|
||||
}
|
||||
|
||||
public function testBadBackgroundColour(): void
|
||||
{
|
||||
$this->expectException(RuntimeException::class);
|
||||
|
||||
new BaconQrCodeProvider(1, '#000', 'not-a-colour');
|
||||
}
|
||||
|
||||
public function testBadTextColourHexRef(): void
|
||||
{
|
||||
$this->expectException(RuntimeException::class);
|
||||
|
||||
new BaconQrCodeProvider(1, '#AAAA', '#FFF');
|
||||
}
|
||||
|
||||
public function testBadBackgroundColourHexRef(): void
|
||||
{
|
||||
$this->expectException(RuntimeException::class);
|
||||
|
||||
new BaconQrCodeProvider(1, '#000', '#AAAA');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace TestsDependency;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use RobThree\Auth\Algorithm;
|
||||
use RobThree\Auth\Providers\Qr\EndroidQrCodeProvider;
|
||||
use RobThree\Auth\Providers\Qr\HandlesDataUri;
|
||||
use RobThree\Auth\TwoFactorAuth;
|
||||
|
||||
class EndroidQRCodeTest extends TestCase
|
||||
{
|
||||
use HandlesDataUri;
|
||||
|
||||
public function testDependency(): void
|
||||
{
|
||||
$qr = new EndroidQrCodeProvider();
|
||||
$tfa = new TwoFactorAuth('Test&Issuer', 6, 30, Algorithm::Sha1, $qr);
|
||||
$data = $this->DecodeDataUri($tfa->getQRCodeImageAsDataUri('Test&Label', 'VMR466AB62ZBOKHE'));
|
||||
$this->assertEquals('image/png', $data['mimetype']);
|
||||
$this->assertEquals('base64', $data['encoding']);
|
||||
$this->assertNotEmpty($data['data']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user