Compare commits

...

142 Commits

Author SHA1 Message Date
Nicolas CARPi 85408c4e77 doc: getting started: improve examples (#154)
add example for default lib qr provider and add issuer too

see #152
2026-01-05 13:17:41 +00:00
Givan 3516aae5a3 Removed curl_close that has no effect since PHP 8.0 to avoid deprecation warning on PHP 8.5 (#155)
Since 8.0 a CurlHandle object is returned, and curl_close() has no effect, so this change won't impact earlier versions.
2026-01-05 13:16:44 +00:00
William Hall a2fa4c11a9 🔧 exclude problematic versions from automated tests 2026-01-05 13:16:07 +00:00
William Hall 57970d3d5f 🔧 update workflows 2026-01-05 13:16:07 +00:00
Will Power 83918a7658 Restore company name in demo (#153) 2025-12-01 13:33:04 +00:00
Rob Janssen 9b2d795acc Merge pull request #149 from dereuromark/patch-1
Fix up readme
2025-04-11 00:41:11 +02:00
Mark Scherer 24f6d2fd10 Fix up readme 2025-04-11 00:25:59 +02:00
YellowMaker 6d70f9ca8e add support for endroid/qr-code (with logo) version 6 (#143) 2024-10-24 16:14:25 +01:00
cliffordvickrey 92487acba4 add support for endroid/qr-code version 6 (#140)
* add support for endroid/qr-code version 6

* add endroid v6 to github workflows

* remove negative conditional (endroid-related)

---------

Co-authored-by: Clifford Vickrey <cvickrey@insuranceautomationgroup.com>
2024-10-24 13:01:17 +01:00
Nicolas CARPi ab4c33007f remove warning about default external qr code provider in README.md
Merge pull request #137 from RobThree/nico-readme
2024-08-20 15:05:21 +02:00
Nicolas CARPi 0ab012bbaf remove warning about default external qr code provider in README.md
In version 3.0, the qrcode provider must be explicitely set, so the
warning is not needed anymore.
Also rewrite the part about the curl library for php.
2024-08-20 13:35:17 +02:00
Will Power ec82d397ee 📚 consistent changelog links to github (#135) 2024-05-27 11:23:05 +01:00
Nicolas CARPi fc3adc7245 mention #130 in changelog 2024-05-17 14:46:27 +02:00
Rob Janssen 337e96b7af Merge pull request #134 from RobThree/nico-coc
add a code of conduct
2024-05-12 14:21:55 +02:00
Nicolas CARPi 91c091c316 add a code of conduct
so we can point to it when needed
2024-05-12 14:06:05 +02:00
Nicolas CARPi 9e8b31ebbc mention #130 in changelog 2024-05-12 13:52:10 +02:00
Nicolas CARPi df43660714 handle curl errors. fix #129
if curl fails for some reason to get a QR code from an external (http) provider, the app will throw a TwoFactorAuthException.

also fix the demo page with new constructor signature
2024-05-07 14:14:34 +02:00
Nicolas CARPi 6194bb08a7 throw a QRException instead
and change the function signature
2024-05-07 03:16:01 +02:00
Nicolas CARPi 6f78141196 handle curl errors. fix #129
if curl fails for some reason to get a QR code from an external (http)
provider, the app will throw a TwoFactorAuthException.

also fix the demo page with new constructor signature
2024-05-07 03:08:31 +02:00
Nicolas CARPi f5eb9a7051 Changelog for 3.x . PR #127 2024-04-28 12:27:24 +02:00
Nicolas CARPi 5c97ce9be8 Update CHANGELOG.md
Co-authored-by: Will Power <1619102+willpower232@users.noreply.github.com>
2024-04-28 00:22:58 +02:00
Nicolas CARPi ab677e43ee Update CHANGELOG.md
propose the use of named arguments

Co-authored-by: Will Power <1619102+willpower232@users.noreply.github.com>
2024-04-28 00:22:41 +02:00
Nicolas CARPi eb02cb7095 fix typos 2024-04-27 19:26:09 +02:00
Nicolas CARPi c15668dc6d Changelog for 3.x 2024-04-27 19:23:24 +02:00
Nicolas CARPi f16ea16e1b set verify ssl parameter for HTTP based QR provider to true by default PR #126 2024-04-27 18:58:46 +02:00
Nicolas CARPi f35f2ae51a make QR Code Provider a mandatory constructor argument PR #125
This change is discussed in #104
Currently, the library defaults to a QR Code Provider using an external service, thus leaking secrets.

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

fixes #104

The public function getQRCodeProvider() has been removed. It is provided by the user in the constructor, so it doesn't make a lot of sense to keep a getter around if we're not using it internally.
2024-04-27 18:58:17 +02:00
Nicolas CARPi cd27effc02 use https protocol for qricket 2024-04-26 18:46:14 +02:00
Nicolas CARPi 2ef7b8e2c6 simplify installation instructions based on Will's comment 2024-04-26 18:45:08 +02:00
Nicolas CARPi b964b2abbf set verify ssl parameter for HTTP based QR provider to true by default
it's already bad to use an external provider, let's use the safest
defaults at least
2024-04-25 21:40:49 +02:00
Nicolas CARPi 8007233058 remove mention of default external provider 2024-04-25 21:31:38 +02:00
Nicolas CARPi 22d45f2ab4 fix documentation link 2024-04-25 21:28:48 +02:00
Nicolas CARPi 2080319f6f make QR Code Provider a mandatory constructor argument
This change is discussed in #104
Currently, the library defaults to a QR Code Provider using an external
service, thus leaking secrets.

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

fixes #104
2024-04-25 21:23:39 +02:00
Will Power cabcf5d493 Merge pull request #124 from RobThree/bacon-has-multiple-versions-now
Bacon has multiple versions now
2024-04-19 12:26:01 +01:00
William Hall 8477141a36 🔧 bump to remove deprecation warnings 2024-04-19 11:40:57 +01:00
William Hall 028867dec2 🚨 bacon has multiple versions now 2024-04-19 11:29:56 +01:00
Rob Janssen dfc1124f96 Merge pull request #117 from Mattie112/patch-1
Changed default secret length from 80bits to 160bits as recommended by RFC4226
2024-04-17 20:46:47 +02:00
Rob Janssen d4a5026d86 Revert back to the intended 160 bits 2024-04-17 20:43:49 +02:00
Rob Janssen bb6802c093 Merge branch 'master' into patch-1 2024-04-17 20:36:16 +02:00
Rob Janssen b909cb38f6 Merge pull request #119 from NicolasCARPi/nico-sensitive-attr
Add SensitiveParameter to sensitive arguments
2024-04-17 20:28:42 +02:00
Nicolas CARPi e08b0a5445 Merge branch 'master' into nico-sensitive-attr
* master:
  add CI4-auth link in README. fix #107 (#123)
  remove insecure rng providers and remove polyfill for hash_equals (#122)
  delete files specific to code editors (#120)
  Exclude useless files from dist archive #103
2024-04-16 18:21:01 +02:00
Nicolas CARPi ecef270ba7 add CI4-auth link in README. fix #107 (#123) 2024-04-16 17:16:48 +01:00
Nicolas CARPi 194ecc28eb remove insecure rng providers and remove polyfill for hash_equals (#122)
* remove insecure rng providers

and remove the openssl provider. We now rely exclusively on
random_bytes(), as there are no reasons not to. Fix #121

* remove the isSecure property of the test rng class

* remove pointless test rng class

we were testing a test class, which didn't make a lot of sense.

* Revert "remove pointless test rng class"

This reverts commit f6da6bee6d.

* Reapply "remove pointless test rng class"

This reverts commit 06220d4a54.

* assing rng provider to class attribute

this also aligns with other providers

* remove polyfill for hash_equals
2024-04-16 16:52:51 +01:00
Nicolas CARPi 323053bd52 delete files specific to code editors (#120)
* delete files specific to code editors

* remove TwoFactorAuth.phpproj
* remove TwoFactorAuth.sln

They are not used anymore. See https://github.com/RobThree/TwoFactorAuth/pull/119#issuecomment-2057777036

* remove deleted files from excluded files in composer.json
2024-04-16 14:44:25 +01:00
Nicolas CARPi 83c7449270 use a link in CHANGELOG.md for SensitiveParameter 2024-04-15 22:43:29 +02:00
Your Name 086a3758ec Exclude useless files from dist archive #103
Co-authored-by: Cédric Anne <cedric.anne@gmail.com>
2024-04-15 21:23:58 +01:00
Nicolas CARPi bba4c207c2 use global import 2024-04-15 21:43:53 +02:00
Nicolas CARPi 061a2a39b4 don't include the sensitive param change in breaking changes section
in changelog
2024-04-15 21:42:23 +02:00
Nicolas CARPi 76f420e6b2 add SensitiveParameter to sensitive arguments
This change adds the PHP attribute SensitiveParameter to the secret
holding variables.
See: https://www.php.net/manual/en/class.sensitiveparameter

This feature is only available in PHP 8.2, so the minimum php version
required has been updated.

Github Actions now use PHP 8.2 and 8.3 for the tests.
The checkout action has been updated to v4, too.

Fix issue #118
2024-04-15 21:40:19 +02:00
Matthijs 319a58a001 Update IRNGProviderTest.php 2024-03-04 16:16:31 +01:00
Matthijs d996779182 Changed default secret bits from 80 to 160 as recommended by RFC4226
https://www.ietf.org/rfc/rfc4226.txt

(and TOTP refers to RFC4226, see https://www.ietf.org/rfc/rfc6238.txt)
2023-11-17 13:53:13 +01:00
Will Power ab93dd41ce Merge pull request #115 from Mattie112/endroidqr-v5
Fix issue #114 (Support for EndroidQR v5)
2023-11-14 12:50:27 +00:00
William Hall ec35073c06 🔧 test all endroid versions we can care about 2023-11-14 12:49:15 +00:00
Matthijs van Schooten 13a56018f4 Fixed typo 2023-11-10 13:43:28 +01:00
Matthijs van Schooten 4b6b0601b7 Fixed check ordering 2023-11-10 13:41:25 +01:00
Matthijs van Schooten 126afe4c25 Removed dependency again from composer 2023-11-10 13:32:44 +01:00
Matthijs van Schooten 63f6259db2 Added support for EndroidQR v5 2023-11-10 13:27:45 +01:00
William Hall 137df4dd3c 🔧 remove deprecated rule https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/pull/7066 2023-07-29 15:46:20 +01:00
Will Power 9476751d6b Merge pull request #110 from modelrailroader/patch-1
Fix docs: Construct new TwoFactorAuth-class
2023-07-29 15:36:31 +01:00
Jan 0565d63e6a fix: better understanding for QrCodeProvider class 2023-06-23 21:21:37 +02:00
Jan d5aa22a96f fix: hyperlinks for new file extensions 2023-06-23 17:56:25 +02:00
Jan a6f7735b9f fix: docs for new PHP Version 2023-06-23 17:54:58 +02:00
Jan 6416e79c3e fix: fixed typing 2023-06-11 12:22:26 +02:00
Jan 83611592db fix: fixed documentation 2023-06-11 12:20:59 +02:00
Jan dd0ecaeed1 Fix: Changes for > PHP 8 2023-06-08 16:56:08 +02:00
Jan 241dfec585 fix: fixed docs 2023-06-05 16:09:42 +02:00
Rob Janssen 63b49ce25c Merge pull request #108 from xHeaven/master
code improvements, readability enhancements, type safety, modern syntax adaptation
2023-06-04 01:03:17 +02:00
Mark Magyar e554a9b5e4 change all $qrtext to camelCase version 2023-05-28 19:47:10 +02:00
Mark Magyar 9bc454f425 use null coalescing assignment instead of just null coalescing 2023-05-28 18:58:39 +02:00
Mark Magyar c2183e16b3 use property promotion for options 2023-05-28 18:46:07 +02:00
Mark Magyar cf84371dad use http_build_query instead of sprintf to improve readability 2023-05-28 01:58:44 +02:00
Mark Magyar 4d91d6ae27 use sprintf instead of concatenating 2023-05-28 01:13:12 +02:00
Mark Magyar d60113a77a drop setAccessible calls as they its NOOP since PHP 8.1 2023-05-27 22:02:24 +02:00
Mark Magyar 3f3327e2a1 use type safe assertions 2023-05-27 22:01:34 +02:00
Mark Magyar 8bf962ecee add readonly identifier 2023-05-27 22:00:48 +02:00
Mark Magyar b80431341e remove pointless +0 2023-05-27 22:00:11 +02:00
Mark Magyar f542788673 fix formatting 2023-05-27 21:59:55 +02:00
Mark Magyar ab51d16de7 remove redundant assignment, use shorthard for nullable type 2023-05-27 21:43:04 +02:00
Mark Magyar d4f6aa1ee6 add missing @throws tag 2023-05-27 21:41:10 +02:00
Mark Magyar 04ff3e7e3f fix nullable return type hint 2023-05-27 21:37:44 +02:00
Mark Magyar d94ee9c769 remove useless @throws because it never throws 2023-05-27 21:36:04 +02:00
Mark Magyar 955ce522f0 add readonly modifiers wherever possible 2023-05-27 21:33:54 +02:00
Mark Magyar e584f0c56a remove unnecessary type casts 2023-05-27 21:30:29 +02:00
Mark Magyar 5949a29b89 use null coalescing operator instead of if checks 2023-05-27 21:29:57 +02:00
Mark Magyar 8bf8800523 use empty string comparison instead of strlen call 2023-05-27 21:29:57 +02:00
Mark Magyar 2fd60fad5e use type safe asserts 2023-05-27 21:29:57 +02:00
Mark Magyar 26413e69f0 extract strlen check from loop 2023-05-27 21:29:57 +02:00
Mark Magyar 3adb3e54e5 add missing delimiter for proper escaping 2023-05-27 21:29:57 +02:00
Mark Magyar c45f7a23c9 fix return types for v3, replace switch with match 2023-05-27 21:29:57 +02:00
Mark Magyar bdfa67d1f6 use static keyword for anonymous function definition 2023-05-27 21:29:57 +02:00
Mark Magyar 7675282f45 put scalar types to the right 2023-05-27 21:29:57 +02:00
Mark Magyar 1ca305ec4a use spread syntax instead of array_merge 2023-05-27 21:29:57 +02:00
Mark Magyar aadc32e85e make parameter explicitly nullable 2023-05-27 21:29:57 +02:00
Mark Magyar 9a1aeb8c1f fix formatting 2023-05-27 20:19:48 +02:00
Mark Magyar 4c8a88224a replace switch with match 2023-05-27 20:14:33 +02:00
Mark Magyar 9523b35d34 use strict comparison 2023-05-27 20:06:37 +02:00
Mark Magyar cc9400d901 fix possible typo in parameter name
Interface's method signature:
```php
public function getQRCodeImage(string $qrtext, int $size): string;
```
2023-05-27 20:05:58 +02:00
Mark Magyar dfb5c1a113 make $borderWidth readonly 2023-05-27 20:04:08 +02:00
William Hall 098dce6735 📚 duplicate default provider warning into docs 2023-04-30 14:36:00 +01:00
Rob Janssen ae1cc5e8af Merge pull request #105 from RMEngels/master
Add warning of default QR Code provider to README.md
2023-04-30 01:27:44 +02:00
RMEngels 6e71f43bb1 Add warning of default QR Code provider to README.md 2023-04-25 11:30:44 +02:00
Will Power 5968be2cee Merge pull request #102 from NicolasCARPi/badges
Fix badges in README
2023-02-27 10:01:43 +00:00
Nicolas CARPi 04e26c9961 remove codeclimate badge because coverage is not uploaded there anyway 2023-02-25 13:07:20 +01:00
Nicolas CARPi 8391c5efb0 fix build badge and codeclimate badge
see: https://github.com/badges/shields/issues/8671
2023-02-25 13:05:01 +01:00
Will Power 27cd1e1392 Merge pull request #97 from NicolasCARPi/php8
Move to PHP 8.1 minimum version, add typing
2023-02-25 11:33:28 +00:00
Will Power 0159e77743 Merge pull request #101 from cedric-anne/composer-archive
Ensure composer dist archive contains only usefull files
2023-02-25 11:33:05 +00:00
Nicolas CARPi 30248a8fb5 address remarks made by MasterOdin 2023-02-22 18:08:19 +01:00
Cédric Anne d6e5e2ef87 Ensure composer dist archive contains only usefull files 2023-02-22 15:42:38 +01:00
Nicolas CARPi ab76ac71a4 use phpunit 9 instead of @stable 2023-02-20 10:31:39 +01:00
Nicolas CARPi 023bfc16f6 add Will Power in authors list in composer.json 2023-02-20 10:29:18 +01:00
Nicolas CARPi afb5cb09be add Nicolas CARPi to authors in composer.json 2023-02-20 02:35:35 +01:00
Will Power 1d628527e5 Merge pull request #98 from thomascorthals/master
Namespace mistake in docs
2023-01-06 14:16:58 +00:00
thomascorthals d4528f58fe Namespace mistake in docs 2023-01-06 12:32:08 +01:00
Nicolas CARPi e6e5d59297 lint 2022-12-29 14:07:44 +01:00
Nicolas CARPi a968dd392a remove the MightNotMakeAssertions shim
use expectNotToPerformAssertions() instead
2022-12-29 14:04:37 +01:00
Nicolas CARPi 86338cf3cd split constructor in multilines 2022-12-29 13:47:06 +01:00
Nicolas CARPi dc4e99e8c3 use @PHP82Migration ruleset 2022-12-29 13:46:05 +01:00
Nicolas CARPi 656e966cdf remove useless function comment 2022-12-29 13:43:52 +01:00
Nicolas CARPi 921425d0ff use psr12 in phpcs config 2022-12-29 13:35:16 +01:00
Nicolas CARPi 4711674ec0 crank phpstan to level 6 2022-12-29 13:32:02 +01:00
Nicolas CARPi e049285b46 use lint-ci for bacon test 2022-12-09 21:46:59 +01:00
Nicolas CARPi b52655b803 add ignore env to php-cs-fixer for ci (php 8.2 issue) 2022-12-09 21:44:02 +01:00
Nicolas CARPi ad89250a8f try adding php8.2 in test matrix 2022-12-09 21:37:29 +01:00
Nicolas CARPi f034bc782e use interface 2022-12-09 19:45:53 +01:00
Nicolas CARPi f8ba3b234b fix endroid qr provider handleErrorCorrectionLevel signature 2022-12-09 19:42:16 +01:00
Nicolas CARPi db6256b5e8 fix endroid handleColor 2022-12-09 19:37:49 +01:00
Nicolas CARPi 041d0e38e2 fix bacon construct signature 2022-12-09 19:36:47 +01:00
Nicolas CARPi 1e75674a45 fix action php version and handleColour function signature for bacon qr 2022-12-09 19:32:27 +01:00
Nicolas CARPi 25f463d19b turns out I had a global gitignore with lib... -_-' 2022-12-09 19:27:50 +01:00
Nicolas CARPi aaf24a66bd why is phpstan failing in actions? 2022-12-09 19:19:24 +01:00
Nicolas CARPi 75d8955967 update the actions 2022-12-09 18:43:15 +01:00
Nicolas CARPi 7e067166aa use relative path for LICENSE link in README
(really it's to trigger an action!)
2022-12-09 18:39:28 +01:00
Nicolas CARPi 58a9628a57 add phpstan config file, ignore broken files because of optional lib
and add it to action
2022-12-09 18:32:43 +01:00
Nicolas CARPi 887f261c88 add CHANGELOG.md 2022-12-09 18:18:55 +01:00
Nicolas CARPi 225216a616 fix php version in readme 2022-12-09 18:12:38 +01:00
Nicolas CARPi 79988ef58a add phpstan 2022-12-08 00:00:18 +01:00
Nicolas CARPi f1e73aab3a fix the testsDependency folder 2022-12-07 23:29:04 +01:00
Nicolas CARPi db0515e826 fix versions in github workflow and add a lint-ci command 2022-12-07 23:14:28 +01:00
Nicolas CARPi 4be83550c0 remove php-parallel-lint: php-cs-fixer does much more 2022-12-07 23:12:17 +01:00
Nicolas CARPi ac721e57b9 add php-cs-fixer 2022-12-07 23:10:13 +01:00
Nicolas CARPi a8c297c178 second batch with QR providers 2022-12-07 23:02:41 +01:00
Nicolas CARPi aeb4b00c60 first batch 2022-12-07 22:28:52 +01:00
William Hall ba4e8c55ed 🔧 don't need version number in composer.json #90 2022-05-10 13:33:17 +01:00
61 changed files with 846 additions and 1383 deletions
+12
View File
@@ -0,0 +1,12 @@
/.github/ export-ignore
/demo/ export-ignore
/docs/ export-ignore
/tests/ export-ignore
/testsDependency/ export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
/.php-cs-fixer.dist.php export-ignore
/logo.png export-ignore
/multifactorauthforeveryone.png export-ignore
/phpstan.neon export-ignore
/phpunit.xml export-ignore
+1
View File
@@ -0,0 +1 @@
Don't be a jerk!
+11 -5
View File
@@ -10,10 +10,16 @@ jobs:
strategy:
matrix:
php-version: ['5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1']
php-version: [8.2, 8.3, 8.4, 8.5]
bacon-version: ["^2", "^3"]
exclude:
- php-version: 8.4
bacon-version: "^2"
- php-version: 8.5
bacon-version: "^2"
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v5
- uses: shivammathur/setup-php@v2
with:
@@ -22,9 +28,9 @@ jobs:
coverage: xdebug
ini-values: error_reporting=E_ALL
- uses: ramsey/composer-install@v1
- uses: ramsey/composer-install@v3
- run: composer require bacon/bacon-qr-code
- run: composer require bacon/bacon-qr-code:${{ matrix.bacon-version }}
- run: composer lint
- run: composer lint-ci
- run: composer test testsDependency/BaconQRCodeTest.php
+19 -21
View File
@@ -10,27 +10,24 @@ jobs:
strategy:
matrix:
php-version: ['8.0', '8.1']
endroid-version: ["^4"]
include:
- php-version: 5.6
# earliest supported version
endroid-version: 2.2.1
- php-version: 7.0
endroid-version: 2.5.1
- php-version: 7.1
# this version is 7.1+
endroid-version: 3.0.0
- php-version: 7.2
# all later versions are 7.3+
endroid-version: 3.5.9
- php-version: 7.3
endroid-version: 3.9.7
- php-version: 7.4
endroid-version: 4.0.0
php-version: [8.2, 8.3, 8.4, 8.5]
endroid-version: ["^3", "^4", "^5", "^6"]
exclude:
- php-version: 8.4
endroid-version: "^3"
- php-version: 8.4
endroid-version: "^4"
- php-version: 8.4
endroid-version: "^5"
- php-version: 8.5
endroid-version: "^3"
- php-version: 8.5
endroid-version: "^4"
- php-version: 8.5
endroid-version: "^5"
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v5
- uses: shivammathur/setup-php@v2
with:
@@ -39,8 +36,9 @@ jobs:
coverage: xdebug
ini-values: error_reporting=E_ALL
- uses: ramsey/composer-install@v1
- uses: ramsey/composer-install@v3
- run: composer require endroid/qrcode:${{ matrix.endroid-version }}
- run: composer require endroid/qrcode:${{ matrix.endroid-version }} -W
- run: composer lint-ci
- run: composer test testsDependency/EndroidQRCodeTest.php
+5 -4
View File
@@ -10,10 +10,10 @@ jobs:
strategy:
matrix:
php-version: ['5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1']
php-version: [8.2, 8.3, 8.4, 8.5]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v5
- uses: shivammathur/setup-php@v2
with:
@@ -22,7 +22,8 @@ jobs:
coverage: xdebug
ini-values: error_reporting=E_ALL
- uses: ramsey/composer-install@v1
- uses: ramsey/composer-install@v3
- run: composer lint
- run: composer lint-ci
- run: composer phpstan
- run: composer test
+1
View File
@@ -190,3 +190,4 @@ composer.lock
.vs/
.phpunit.result.cache
.php-cs-fixer.cache
+60
View File
@@ -0,0 +1,60 @@
<?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_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)
;
+71
View File
@@ -0,0 +1,71 @@
# RobThree\TwoFactorAuth changelog
# Version 3.x
## Breaking changes
### PHP Version
Version 3.x requires at least PHP 8.2.
### Constructor signature change
In order to ensure users of this library make a conscious choice of QR Code Provider, the QR Code Provider is now a mandatory argument, in first place.
If you didn't provide one explicitly before, you can get the old behavior with:
~~~php
use RobThree\Auth\TwoFactorAuth;
use RobThree\Auth\Providers\Qr\QRServerProvider;
$tfa = new TwoFactorAuth(new QRServerProvider());
~~~
If you provided one before, the order of the parameters have been changed, so simply move the QRCodeProvider argument to the first place or use named arguments.
Documentation on selecting a QR Code Provider is available here: [QR Code Provider documentation](https://robthree.github.io/TwoFactorAuth/qr-codes.html).
### Default secret length
The default secret length has been increased from 80 bits to 160 bits (RFC4226) PR [#117](https://github.com/RobThree/TwoFactorAuth/pull/117). This might cause an issue in your application if you were previously storing secrets in a column with restricted size. This change doesn't impact existing secrets, only new ones will get longer.
Previously a secret was 16 characters, now it needs to be stored in a 32 characters width column.
You can keep the old behavior by setting `80` as argument to `createSecret()` (not recommended, see [#117](https://github.com/RobThree/TwoFactorAuth/pull/117) for further discussion).
## Other changes
* The new PHP attribute [SensitiveParameter](https://www.php.net/manual/en/class.sensitiveparameter.php) was added to the code, to prevent accidental leak of secrets in stack traces.
* Likely not breaking anything, but now all external QR Code providers use HTTPS with a verified certificate. PR [#126](https://github.com/RobThree/TwoFactorAuth/pull/126).
* The CSPRNG is now exclusively using `random_bytes()` PHP function. Previously a fallback to `openssl` or non cryptographically secure PRNG existed, they have been removed. PR [#122](https://github.com/RobThree/TwoFactorAuth/pull/122).
* If an external QR code provider is used and the HTTP request results in an error, it will throw a `QRException`. Previously the error was ignored. PR [#130](https://github.com/RobThree/TwoFactorAuth/pull/130), fixes [#129](https://github.com/RobThree/TwoFactorAuth/issues/129).
# 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.
+6 -6
View File
@@ -1,6 +1,6 @@
# ![Logo](https://raw.githubusercontent.com/RobThree/TwoFactorAuth/master/logo.png) PHP library for Two Factor Authentication
[![Build status](https://img.shields.io/github/workflow/status/RobThree/TwoFactorAuth/Test/master?style=flat-square)](https://github.com/RobThree/TwoFactorAuth/actions?query=branch%3Amaster) [![Latest Stable Version](https://img.shields.io/packagist/v/robthree/twofactorauth.svg?style=flat-square)](https://packagist.org/packages/robthree/twofactorauth) [![License](https://img.shields.io/packagist/l/robthree/twofactorauth.svg?style=flat-square)](LICENSE) [![Downloads](https://img.shields.io/packagist/dt/robthree/twofactorauth.svg?style=flat-square)](https://packagist.org/packages/robthree/twofactorauth) [![Code Climate](https://img.shields.io/codeclimate/github/RobThree/TwoFactorAuth.svg?style=flat-square)](https://codeclimate.com/github/RobThree/TwoFactorAuth) [![PayPal donate button](http://img.shields.io/badge/paypal-donate-orange.svg?style=flat-square)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=6MB5M2SQLP636 "Keep me off the streets")
[![Build status](https://img.shields.io/github/actions/workflow/status/robthree/twofactorauth/test.yml?branch=master)](https://github.com/RobThree/TwoFactorAuth/actions?query=branch%3Amaster) [![Latest Stable Version](https://img.shields.io/packagist/v/robthree/twofactorauth.svg?style=flat-square)](https://packagist.org/packages/robthree/twofactorauth) [![License](https://img.shields.io/packagist/l/robthree/twofactorauth.svg?style=flat-square)](LICENSE) [![Downloads](https://img.shields.io/packagist/dt/robthree/twofactorauth.svg?style=flat-square)](https://packagist.org/packages/robthree/twofactorauth) [![PayPal donate button](http://img.shields.io/badge/paypal-donate-orange.svg?style=flat-square)](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,15 +10,14 @@ PHP library for [two-factor (or multi-factor) authentication](http://en.wikipedi
## Requirements
* Tested on PHP 5.6 up to 8.0
* [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), [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.2
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`.
* [php-curl library](http://php.net/manual/en/book.curl.php) when using an external QR Code provider such as `QRServerProvider`, `ImageChartsQRCodeProvider`, `QRicketProvider` or any other custom provider connecting to an external service.
## Installation
@@ -34,10 +33,11 @@ If you need more in-depth information about the configuration available then you
## Integrations
- [CakePHP 3](https://github.com/andrej-griniuk/cakephp-two-factor-auth)
- [CakePHP plugin](https://github.com/andrej-griniuk/cakephp-two-factor-auth)
- [CI4-Auth: a user, group, role and permission management library for Codeigniter 4](https://github.com/glewe/ci4-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))
-70
View File
@@ -1,70 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Name>TwoFactorAuth</Name>
<ProjectGuid>{e569f53a-a604-4579-91ce-4e35b27da47b}</ProjectGuid>
<RootNamespace>TwoFactorAuth</RootNamespace>
<OutputType>Library</OutputType>
<ProjectTypeGuids>{A0786B88-2ADB-4C21-ABE8-AA2D79766269}</ProjectTypeGuids>
<SaveServerSettingsInUserFile>False</SaveServerSettingsInUserFile>
<Server>PHPDev</Server>
<PublishEvent>None</PublishEvent>
<PHPDevAutoPort>True</PHPDevAutoPort>
<PHPDevPort>41315</PHPDevPort>
<PHPDevHostName>localhost</PHPDevHostName>
<IISProjectUrl>http://localhost:41315/</IISProjectUrl>
<Runtime>PHP</Runtime>
<RuntimeVersion>7.0</RuntimeVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<IncludeDebugInformation>true</IncludeDebugInformation>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<IncludeDebugInformation>false</IncludeDebugInformation>
</PropertyGroup>
<ItemGroup>
<Compile Include="demo\demo.php" />
<Compile Include="demo\loader.php" />
<Compile Include="lib\Providers\Qr\BaseHTTPQRCodeProvider.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" />
<Compile Include="lib\TwoFactorAuthException.php" />
<Compile Include="tests\TwoFactorAuthTest.php" />
</ItemGroup>
<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\" />
<Folder Include="tests\" />
</ItemGroup>
<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>
-22
View File
@@ -1,22 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2013
VisualStudioVersion = 12.0.30723.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{A0786B88-2ADB-4C21-ABE8-AA2D79766269}") = "TwoFactorAuth", "TwoFactorAuth.phpproj", "{E569F53A-A604-4579-91CE-4E35B27DA47B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{E569F53A-A604-4579-91CE-4E35B27DA47B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E569F53A-A604-4579-91CE-4E35B27DA47B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E569F53A-A604-4579-91CE-4E35B27DA47B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E569F53A-A604-4579-91CE-4E35B27DA47B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
+21 -5
View File
@@ -1,7 +1,6 @@
{
"name": "robthree/twofactorauth",
"description": "Two Factor Authentication",
"version": "1.8.2",
"type": "library",
"keywords": [ "Authentication", "Two Factor Authentication", "Multi Factor Authentication", "TFA", "MFA", "PHP", "Authenticator", "Authy" ],
"homepage": "https://github.com/RobThree/TwoFactorAuth",
@@ -11,6 +10,16 @@
"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": {
@@ -18,11 +27,12 @@
"source": "https://github.com/RobThree/TwoFactorAuth"
},
"require": {
"php": ">=5.6.0"
"php": ">=8.2.0"
},
"require-dev": {
"phpunit/phpunit": "@stable",
"php-parallel-lint/php-parallel-lint": "^1.2"
"phpunit/phpunit": "^9",
"friendsofphp/php-cs-fixer": "^3.13",
"phpstan/phpstan": "^1.9"
},
"suggest": {
"bacon/bacon-qr-code": "Needed for BaconQrCodeProvider provider",
@@ -39,8 +49,14 @@
}
},
"scripts": {
"phpstan": [
"phpstan analyze --xdebug lib tests testsDependency"
],
"lint": [
"parallel-lint --exclude vendor ."
"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"
+4 -2
View File
@@ -11,8 +11,10 @@
include_once str_replace(array('RobThree\\Auth', '\\'), array(__DIR__.'/../lib', '/'), $className) . '.php';
});
// substitute your company or app name here
$tfa = new RobThree\Auth\TwoFactorAuth('RobThree TwoFactorAuth');
$tfa = new RobThree\Auth\TwoFactorAuth(
new RobThree\Auth\Providers\Qr\QRServerProvider(),
"Acme Corp" // substitute your company or app name here
);
?>
<li>First create a secret and associate it with a user</li>
<?php
+18 -10
View File
@@ -7,28 +7,36 @@ title: Getting Started
The best way of making use of this project is by installing it with [composer](https://getcomposer.org/doc/01-basic-usage.md).
```
php composer.phar require robthree/twofactorauth
```
or if you have composer installed globally
```
composer require robthree/twofactorauth
```
## 2. Create an instance
Now you can create an instance for use with your code
`TwoFactorAuth` constructor requires an object able to provide a QR Code image. It is the only mandatory argument. This lets you select your preferred QR Code generator/library.
See [QR code providers documentation](qr-codes.md) for more information about the different possibilites.
Example code:
```php
use RobThree\Auth\TwoFactorAuth;
use RobThree\Auth\Providers\Qr\QRServerProvider; // if using library's provider
use RobThree\Auth\Providers\Qr\BaconQrCodeProvider; // if using Bacon
use RobThree\Auth\Providers\Qr\EndroidQrCodeProvider; // if using Endroid
$tfa = new TwoFactorAuth();
// using the default Qr Code provider from the library
$tfa = new TwoFactorAuth(new QRServerProvider(), "Your app name");
// using Bacon
$tfa = new TwoFactorAuth(new BaconQrCodeProvider(), "Your app name");
// using Endroid
$tfa = new TwoFactorAuth(new EndroidQrCodeProvider(), "Your app name");
// using a custom object implementing IQRCodeProvider interface
$tfa = new TwoFactorAuth(new MyQrCodeProvider(), "Your app name");
// using named argument and a variable
$tfa = new TwoFactorAuth(qrcodeprovider: $qrGenerator, issuer: "Your app name");
```
**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.
+10 -19
View File
@@ -7,30 +7,21 @@ title: Optional 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
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` | `Algorithm::Sha1` | The algorithm used (one of `Algorithm::Sha1`, `Algorithm::Sha256`, `Algorithm::Sha512`, `Algorithm::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. [MCryptRNGProvider](https://github.com/RobThree/TwoFactorAuth/blob/master/lib/Providers/Rng/MCryptRNGProvider.php) where mcrypt is available
3. [OpenSSLRNGProvider](https://github.com/RobThree/TwoFactorAuth/blob/master/lib/Providers/Rng/OpenSSLRNGProvider.php) where openssl is available
4. [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).
Should you feel the need to use a CSPRNG different than `random_bytes()`, you can use the `rngprovider` argument of the constructor to provide an object implementing the [`IRNGProvider`](https://github.com/RobThree/TwoFactorAuth/blob/master/lib/Providers/Rng/IRNGProvider.php) interface.
### Time providers
+25 -28
View File
@@ -5,7 +5,7 @@ title: QR Codes
An alternative way of communicating the secret to the user is through the use of [QR Codes](http://en.wikipedia.org/wiki/QR_code) which most if not all authenticator mobile apps can scan.
This can avoid accidental typing errors and also pre-set some text values within the users app.
This can avoid accidental typing errors and also pre-set some text values within the two factor authentication mobile application.
You can display the QR Code as a base64 encoded image using the instance as follows, supplying the users name or other public identifier as the first argument
@@ -16,46 +16,43 @@ You can display the QR Code as a base64 encoded image using the instance as foll
You can also specify a size as a third argument which is 200 by default.
**Note:** by default, the QR code returned by the instance is generated from a third party across the internet. If the third party is encountering problems or is not available from where you have hosted your code, your user will likely experience a delay in seeing the QR code, if it even loads at all. This can be overcome with offline providers configured when you create the instance.
## Online Providers
[QRServerProvider](qr-codes/qr-server.html) (default)
[ImageChartsQRCodeProvider](qr-codes/image-charts.html)
[QRicketProvider](qr-codes/qrickit.html)
## Offline Providers
[EndroidQrCodeProvider](qr-codes/endroid.html) and EndroidQrCodeWithLogoProvider
[EndroidQrCodeProvider](qr-codes/endroid.md) and EndroidQrCodeWithLogoProvider
[BaconQRCodeProvider](qr-codes/bacon.html)
[BaconQRCodeProvider](qr-codes/bacon.md)
**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).
If you wish to make your own QR Code provider to reference another service or library, it must implement the [IQRCodeProvider interface](../lib/Providers/Qr/IQRCodeProvider.php).
It is recommended to use similar constructor arguments as the included providers to avoid big shifts when trying different providers.
## Using a specific provider
If you do not want to use the default QR code provider, you can specify the one you want to use when you create your instance.
Example:
```php
use RobThree\Auth\TwoFactorAuth;
$qrCodeProvider = new YourChosenProvider();
$tfa = new TwoFactorAuth(
null,
6,
30,
'sha1',
$qrCodeProvider
);
// using a custom object implementing IQRCodeProvider
$tfa = new TwoFactorAuth(new MyQrCodeProvider());
// using named argument and a variable
$tfa = new TwoFactorAuth(qrcodeprovider: $qrGenerator);
```
As you create a new instance of your provider, you can supply any extra configuration there.
## Online Providers
**Warning:** Using an external service for generating QR codes encoding authentication secrets is **not** recommended! You should instead make use of the included offline providers listed above.
* Gogr.me: [QRServerProvider](qr-codes/qr-server.md)
* Image Charts: [ImageChartsQRCodeProvider](qr-codes/image-charts.md)
* Qrickit: [QRicketProvider](qr-codes/qrickit.md)
* Google Charts: [GoogleChartsQrCodeProvider](qr-codes/google-charts.md)
Example:
```php
use RobThree\Auth\TwoFactorAuth;
use RobThree\Auth\Providers\Qr\GoogleChartsQrCodeProvider;
$tfa = new TwoFactorAuth(new GoogleChartsQrCodeProvider());
```
+1 -1
View File
@@ -27,7 +27,7 @@ Argument | Default value
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\TwoFactorAuth\Providers\Qr\EndroidQrCodeWithLogoProvider;
use RobThree\Auth\Providers\Qr\EndroidQrCodeWithLogoProvider;
$qrCodeProvider = new EndroidQrCodeWithLogoProvider();
+15
View File
@@ -0,0 +1,15 @@
---
layout: post
title: QR GoogleCharts
---
See: https://developers.google.com/chart/infographics/docs/qr_codes
## Optional Configuration
Argument | Default value
------------------------|---------------
`$verifyssl` | `false`
`$errorcorrectionlevel` | `'L'`
`$margin` | `4`
`$encoding` | `'UTF-8'`
+1 -1
View File
@@ -7,7 +7,7 @@ title: Image-Charts
Argument | Default value
------------------------|---------------
`$verifyssl` | `false`
`$verifyssl` | `true`
`$errorcorrectionlevel` | `'L'`
`$margin` | `4`
+1 -1
View File
@@ -7,7 +7,7 @@ title: QR Server
Argument | Default value
------------------------|---------------
`$verifyssl` | `false`
`$verifyssl` | `true`
`$errorcorrectionlevel` | `'L'`
`$margin` | `4`
`$qzone` | `1`
+16
View File
@@ -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';
}
+33 -51
View File
@@ -1,51 +1,38 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth\Providers\Qr;
use BaconQrCode\Writer;
use BaconQrCode\Renderer\ImageRenderer;
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
use BaconQrCode\Renderer\RendererStyle\Fill;
use BaconQrCode\Renderer\Color\Rgb;
use BaconQrCode\Renderer\RendererStyle\EyeFill;
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
{
private $borderWidth = 4; // default from Bacon QR Code
private $backgroundColour;
private $foregroundColour;
private $format;
/**
* Ensure we using the latest Bacon QR Code and specify default options
*
* @param int $borderWidth space around the QR code, 4 is the default from Bacon QR Code
* @param string $backgroundColour hex reference for the background colour
* @param string $foregroundColour hex reference for the foreground colour
* @param string $format the desired output, png or svg
*/
public function __construct($borderWidth = 4, $backgroundColour = '#ffffff', $foregroundColour = '#000000', $format = 'png')
{
if (! class_exists(ImagickImageBackEnd::class)) {
throw new \RuntimeException('Make sure you are using version 2 of Bacon QR Code');
}
$this->borderWidth = $borderWidth;
$this->backgroundColour = $this->handleColour($backgroundColour);
$this->foregroundColour = $this->handleColour($foregroundColour);
$this->format = strtolower($format);
public function __construct(
private readonly 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);
}
/**
* Standard functions from IQRCodeProvider
*/
public function getMimeType()
public function getMimeType(): string
{
switch ($this->format) {
case 'png':
@@ -61,25 +48,20 @@ class BaconQrCodeProvider implements IQRCodeProvider
return 'application/postscript';
}
throw new \RuntimeException(sprintf('Unknown MIME-type: %s', $this->format));
throw new RuntimeException(sprintf('Unknown MIME-type: %s', $this->format));
}
public function getQRCodeImage($qrText, $size)
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);
}
$backend = match ($this->format) {
'svg' => new SvgImageBackEnd(),
'eps' => new EpsImageBackEnd(),
default => new ImagickImageBackEnd($this->format),
};
$output = $this->getQRCodeByBackend($qrText, $size, $backend);
if ($this->format == 'svg') {
if ($this->format === 'svg') {
$svg = explode("\n", $output);
return $svg[1];
}
@@ -96,7 +78,7 @@ class BaconQrCodeProvider implements IQRCodeProvider
$rendererStyleArgs = array($size, $this->borderWidth);
if (is_array($this->foregroundColour) && is_array($this->backgroundColour)) {
$rendererStyleArgs = array_merge($rendererStyleArgs, array(
$rendererStyleArgs = array(...$rendererStyleArgs, ...array(
null,
null,
Fill::withForegroundColor(
@@ -105,7 +87,7 @@ class BaconQrCodeProvider implements IQRCodeProvider
new EyeFill(null, null),
new EyeFill(null, null),
new EyeFill(null, null)
)
),
));
}
@@ -121,16 +103,16 @@ class BaconQrCodeProvider implements IQRCodeProvider
* 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($colour)
private function handleColour(array|string $colour): array|string
{
if (is_string($colour) && $colour[0] == '#') {
$hexToRGB = function ($input) {
$hexToRGB = static 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 #');
throw new RuntimeException('Colour should be a 3 or 6 character value after the #');
}
// split the array into three chunks
@@ -138,7 +120,7 @@ class BaconQrCodeProvider implements IQRCodeProvider
// cope with three character hex reference
if (strlen($input) == 3) {
array_walk($split, function (&$character) {
array_walk($split, static function (&$character) {
$character = str_repeat($character, 2);
});
}
@@ -154,6 +136,6 @@ class BaconQrCodeProvider implements IQRCodeProvider
return $colour;
}
throw new \RuntimeException('Invalid colour value');
throw new RuntimeException('Invalid colour value');
}
}
+8 -10
View File
@@ -1,18 +1,14 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth\Providers\Qr;
abstract class BaseHTTPQRCodeProvider implements IQRCodeProvider
{
/** @var bool */
protected $verifyssl;
protected bool $verifyssl = true;
/**
* @param string $url
*
* @return string|bool
*/
protected function getContent($url)
protected function getContent(string $url): string
{
$curlhandle = curl_init();
@@ -23,11 +19,13 @@ 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);
if ($data === false) {
throw new QRException(curl_error($curlhandle));
}
curl_close($curlhandle);
return $data;
}
}
+60 -21
View File
@@ -1,9 +1,13 @@
<?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;
@@ -13,15 +17,24 @@ use Endroid\QrCode\Writer\PngWriter;
class EndroidQrCodeProvider implements IQRCodeProvider
{
public $bgcolor;
public $color;
public $margin;
public $errorcorrectionlevel;
protected $endroid4 = false;
protected $endroid5 = false;
protected $endroid6 = false;
public function __construct($bgcolor = 'ffffff', $color = '000000', $margin = 0, $errorcorrectionlevel = 'H')
{
$this->endroid4 = method_exists(QrCode::class, 'create');
$this->endroid5 = enum_exists(ErrorCorrectionLevel::class);
$this->endroid6 = $this->endroid5 && !method_exists(QrCode::class, 'setSize');
$this->endroid4 = $this->endroid6 || method_exists(QrCode::class, 'create');
$this->bgcolor = $this->handleColor($bgcolor);
$this->color = $this->handleColor($color);
@@ -29,56 +42,82 @@ class EndroidQrCodeProvider implements IQRCodeProvider
$this->errorcorrectionlevel = $this->handleErrorCorrectionLevel($errorcorrectionlevel);
}
public function getMimeType()
public function getMimeType(): string
{
return 'image/png';
}
public function getQRCodeImage($qrtext, $size)
public function getQRCodeImage(string $qrText, int $size): string
{
if (!$this->endroid4) {
return $this->qrCodeInstance($qrtext, $size)->writeString();
return $this->qrCodeInstance($qrText, $size)->writeString();
}
$writer = new PngWriter();
return $writer->write($this->qrCodeInstance($qrtext, $size))->getString();
return $writer->write($this->qrCodeInstance($qrText, $size))->getString();
}
protected function qrCodeInstance($qrtext, $size)
protected function qrCodeInstance(string $qrText, int $size): QrCode
{
$qrCode = new QrCode($qrtext);
if ($this->endroid6) {
return new QrCode(
data: $qrText,
errorCorrectionLevel: $this->errorcorrectionlevel,
size: $size,
margin: $this->margin,
foregroundColor: $this->color,
backgroundColor: $this->bgcolor
);
}
$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($color)
private function handleColor(string $color): Color|array
{
$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) : ['r' => $r, 'g' => $g, 'b' => $b, 'a' => 0];
return $this->endroid4 ? new Color($r, $g, $b, 0) : array('r' => $r, 'g' => $g, 'b' => $b, 'a' => 0);
}
private function handleErrorCorrectionLevel($level)
private function handleErrorCorrectionLevel(string $level): ErrorCorrectionLevelInterface|ErrorCorrectionLevel
{
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();
// First check for version 5 (using enums)
if ($this->endroid5) {
return match ($level) {
'L' => ErrorCorrectionLevel::Low,
'M' => ErrorCorrectionLevel::Medium,
'Q' => ErrorCorrectionLevel::Quartile,
default => ErrorCorrectionLevel::High,
};
}
// If not check for version 4 (using classes)
if ($this->endroid4) {
return match ($level) {
'L' => new ErrorCorrectionLevelLow(),
'M' => new ErrorCorrectionLevelMedium(),
'Q' => new ErrorCorrectionLevelQuartile(),
default => new ErrorCorrectionLevelHigh(),
};
}
// Any other version will be using strings
return match ($level) {
'L' => ErrorCorrectionLevel::LOW(),
'M' => ErrorCorrectionLevel::MEDIUM(),
'Q' => ErrorCorrectionLevel::QUARTILE(),
default => ErrorCorrectionLevel::HIGH(),
};
}
}
@@ -1,12 +1,17 @@
<?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;
/**
@@ -20,33 +25,38 @@ class EndroidQrCodeWithLogoProvider extends EndroidQrCodeProvider
$this->logoSize = (array)$size;
}
public function getQRCodeImage($qrtext, $size)
public function getQRCodeImage(string $qrText, int $size): string
{
if (!$this->endroid4) {
return $this->qrCodeInstance($qrtext, $size)->writeString();
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]);
if ($this->endroid6) {
$logo = new Logo($this->logoPath, ...$this->logoSize);
} else {
$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();
return $writer->write($this->qrCodeInstance($qrText, $size), $logo)->getString();
}
protected function qrCodeInstance($qrtext, $size) {
$qrCode = parent::qrCodeInstance($qrtext, $size);
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], isset($this->logoSize[1]) ? $this->logoSize[1] : null);
$qrCode->setLogoSize($this->logoSize[0], $this->logoSize[1] ?? null);
}
}
+17 -48
View File
@@ -1,67 +1,36 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth\Providers\Qr;
// https://developers.google.com/chart/infographics/docs/qr_codes
class GoogleChartsQrCodeProvider extends BaseHTTPQRCodeProvider
{
/** @var string */
public $errorcorrectionlevel;
/** @var int */
public $margin;
/** @var string */
public $encoding;
/**
* @param bool $verifyssl
* @param string $errorcorrectionlevel
* @param int $margin
* @param string $encoding
*/
public function __construct($verifyssl = false, $errorcorrectionlevel = 'L', $margin = 4, $encoding = 'UTF-8')
public function __construct(protected bool $verifyssl = true, public string $errorcorrectionlevel = 'L', public int $margin = 4, public string $encoding = 'UTF-8')
{
if (!is_bool($verifyssl)) {
throw new QRException('VerifySSL must be bool');
}
$this->verifyssl = $verifyssl;
$this->errorcorrectionlevel = $errorcorrectionlevel;
$this->margin = $margin;
$this->encoding = $encoding;
}
/**
* {@inheritdoc}
*/
public function getMimeType()
public function getMimeType(): string
{
return 'image/png';
}
/**
* {@inheritdoc}
*/
public function getQRCodeImage($qrtext, $size)
public function getQRCodeImage(string $qrText, int $size): string
{
return $this->getContent($this->getUrl($qrtext, $size));
return $this->getContent($this->getUrl($qrText, $size));
}
/**
* @param string $qrtext the value to encode in the QR code
* @param int|string $size the desired size of the QR code
*
* @return string file contents of the QR code
*/
public function 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);
$queryParameters = array(
'chs' => $size . 'x' . $size,
'chld' => strtoupper($this->errorcorrectionlevel) . '|' . $this->margin,
'cht' => 'qr',
'choe' => $this->encoding,
'chl' => $qrText,
);
return 'https://chart.googleapis.com/chart?' . http_build_query($queryParameters);
}
}
+9 -6
View File
@@ -1,21 +1,24 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth\Providers\Qr;
use function base64_decode;
use function preg_match;
trait HandlesDataUri
{
/**
* @param string $datauri
*
* @return null|array
/**
* @return array<string, string>|null
*/
private function DecodeDataUri($datauri)
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'])
'data' => base64_decode($m['data'], true),
);
}
+5 -5
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth\Providers\Qr;
interface IQRCodeProvider
@@ -7,18 +9,16 @@ interface IQRCodeProvider
/**
* Generate and return the QR code to embed in a web page
*
* @param string $qrtext the value to encode in the QR code
* @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($qrtext, $size);
public function getQRCodeImage(string $qrText, int $size): string;
/**
* Returns the appropriate mime type for the QR code
* that will be generated
*
* @return string
*/
public function getMimeType();
public function getMimeType(): string;
}
+18 -41
View File
@@ -1,60 +1,37 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth\Providers\Qr;
// https://image-charts.com
/**
* Use https://image-charts.com to provide a QR code
*/
class ImageChartsQRCodeProvider extends BaseHTTPQRCodeProvider
{
/** @var string */
public $errorcorrectionlevel;
/** @var int */
public $margin;
/**
* @param bool $verifyssl
* @param string $errorcorrectionlevel
* @param int $margin
*/
public function __construct($verifyssl = false, $errorcorrectionlevel = 'L', $margin = 1)
public function __construct(protected bool $verifyssl = true, public string $errorcorrectionlevel = 'L', public int $margin = 1)
{
if (!is_bool($verifyssl)) {
throw new QRException('VerifySSL must be bool');
}
$this->verifyssl = $verifyssl;
$this->errorcorrectionlevel = $errorcorrectionlevel;
$this->margin = $margin;
}
/**
* {@inheritdoc}
*/
public function getMimeType()
public function getMimeType(): string
{
return 'image/png';
}
/**
* {@inheritdoc}
*/
public function getQRCodeImage($qrtext, $size)
public function getQRCodeImage(string $qrText, int $size): string
{
return $this->getContent($this->getUrl($qrtext, $size));
return $this->getContent($this->getUrl($qrText, $size));
}
/**
* @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 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);
$queryParameters = array(
'cht' => 'qr',
'chs' => ceil($size / 2) . 'x' . ceil($size / 2),
'chld' => $this->errorcorrectionlevel . '|' . $this->margin,
'chl' => $qrText,
);
return 'https://image-charts.com/chart?' . http_build_query($queryParameters);
}
}
+5 -1
View File
@@ -1,7 +1,11 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth\Providers\Qr;
use RobThree\Auth\TwoFactorAuthException;
class QRException extends TwoFactorAuthException {}
class QRException extends TwoFactorAuthException
{
}
+24 -73
View File
@@ -1,57 +1,19 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth\Providers\Qr;
// http://goqr.me/api/doc/create-qr-code/
/**
* Use https://goqr.me/api/doc/create-qr-code/ to get QR code
*/
class QRServerProvider extends BaseHTTPQRCodeProvider
{
/** @var string */
public $errorcorrectionlevel;
/** @var int */
public $margin;
/** @var int */
public $qzone;
/** @var string */
public $bgcolor;
/** @var string */
public $color;
/** @var string */
public $format;
/**
* @param bool $verifyssl
* @param string $errorcorrectionlevel
* @param int $margin
* @param int $qzone
* @param string $bgcolor
* @param string $color
* @param string $format
*/
public function __construct($verifyssl = false, $errorcorrectionlevel = 'L', $margin = 4, $qzone = 1, $bgcolor = 'ffffff', $color = '000000', $format = 'png')
public function __construct(protected bool $verifyssl = true, 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;
}
/**
* {@inheritdoc}
*/
public function getMimeType()
public function getMimeType(): string
{
switch (strtolower($this->format)) {
case 'png':
@@ -69,40 +31,29 @@ class QRServerProvider extends BaseHTTPQRCodeProvider
throw new QRException(sprintf('Unknown MIME-type: %s', $this->format));
}
/**
* {@inheritdoc}
*/
public function getQRCodeImage($qrtext, $size)
public function getQRCodeImage(string $qrText, int $size): string
{
return $this->getContent($this->getUrl($qrtext, $size));
return $this->getContent($this->getUrl($qrText, $size));
}
/**
* @param string $value
*
* @return string
*/
private function decodeColor($value)
public function getUrl(string $qrText, int $size): string
{
return vsprintf('%d-%d-%d', sscanf($value, "%02x%02x%02x"));
$queryParameters = array(
'size' => $size . 'x' . $size,
'ecc' => strtoupper($this->errorcorrectionlevel),
'margin' => $this->margin,
'qzone' => $this->qzone,
'bgcolor' => $this->decodeColor($this->bgcolor),
'color' => $this->decodeColor($this->color),
'format' => strtolower($this->format),
'data' => $qrText,
);
return 'https://api.qrserver.com/v1/create-qr-code/?' . http_build_query($queryParameters);
}
/**
* @param string $qrtext the value to encode in the QR code
* @param int|string $size the desired size of the QR code
*
* @return string file contents of the QR code
*/
public function getUrl($qrtext, $size)
private function decodeColor(string $value): string
{
return 'https://api.qrserver.com/v1/create-qr-code/'
. '?size=' . $size . 'x' . $size
. '&ecc=' . strtoupper($this->errorcorrectionlevel)
. '&margin=' . $this->margin
. '&qzone=' . $this->qzone
. '&bgcolor=' . $this->decodeColor($this->bgcolor)
. '&color=' . $this->decodeColor($this->color)
. '&format=' . strtolower($this->format)
. '&data=' . rawurlencode($qrtext);
return vsprintf('%d-%d-%d', sscanf($value, '%02x%02x%02x'));
}
}
+20 -49
View File
@@ -1,42 +1,19 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth\Providers\Qr;
// http://qrickit.com/qrickit_apps/qrickit_api.php
/**
* Use http://qrickit.com/qrickit_apps/qrickit_api.php to provide a QR code
*/
class QRicketProvider extends BaseHTTPQRCodeProvider
{
/** @var string */
public $errorcorrectionlevel;
/** @var string */
public $bgcolor;
/** @var string */
public $color;
/** @var string */
public $format;
/**
* @param string $errorcorrectionlevel
* @param string $bgcolor
* @param string $color
* @param string $format
*/
public function __construct($errorcorrectionlevel = 'L', $bgcolor = 'ffffff', $color = '000000', $format = 'p')
public function __construct(protected bool $verifyssl = true, 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;
}
/**
* {@inheritdoc}
*/
public function getMimeType()
public function getMimeType(): string
{
switch (strtolower($this->format)) {
case 'p':
@@ -49,28 +26,22 @@ class QRicketProvider extends BaseHTTPQRCodeProvider
throw new QRException(sprintf('Unknown MIME-type: %s', $this->format));
}
/**
* {@inheritdoc}
*/
public function getQRCodeImage($qrtext, $size)
public function getQRCodeImage(string $qrText, int $size): string
{
return $this->getContent($this->getUrl($qrtext, $size));
return $this->getContent($this->getUrl($qrText, $size));
}
/**
* @param string $qrtext the value to encode in the QR code
* @param int|string $size the desired size of the QR code
*
* @return string file contents of the QR code
*/
public function getUrl($qrtext, $size)
public function getUrl(string $qrText, int $size): string
{
return 'http://qrickit.com/api/qr'
. '?qrsize=' . $size
. '&e=' . strtolower($this->errorcorrectionlevel)
. '&bgdcolor=' . $this->bgcolor
. '&fgdcolor=' . $this->color
. '&t=' . strtolower($this->format)
. '&d=' . rawurlencode($qrtext);
$queryParameters = array(
'qrsize' => $size,
'e' => strtolower($this->errorcorrectionlevel),
'bgdcolor' => $this->bgcolor,
'fgdcolor' => $this->color,
't' => strtolower($this->format),
'd' => $qrText,
);
return 'https://qrickit.com/api/qr?' . http_build_query($queryParameters);
}
}
+3 -9
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth\Providers\Rng;
class CSRNGProvider implements IRNGProvider
@@ -7,16 +9,8 @@ class CSRNGProvider implements IRNGProvider
/**
* {@inheritdoc}
*/
public function getRandomBytes($bytecount)
public function getRandomBytes(int $bytecount): string
{
return random_bytes($bytecount); // PHP7+
}
/**
* {@inheritdoc}
*/
public function isCryptographicallySecure()
{
return true;
}
}
-43
View File
@@ -1,43 +0,0 @@
<?php
namespace RobThree\Auth\Providers\Rng;
class HashRNGProvider implements IRNGProvider
{
/** @var string */
private $algorithm;
/**
* @param string $algorithm
*/
public function __construct($algorithm = 'sha256')
{
$algos = array_values(hash_algos());
if (!in_array($algorithm, $algos, true)) {
throw new RNGException('Unsupported algorithm specified');
}
$this->algorithm = $algorithm;
}
/**
* {@inheritdoc}
*/
public function getRandomBytes($bytecount)
{
$result = '';
$hash = mt_rand();
for ($i = 0; $i < $bytecount; $i++) {
$hash = hash($this->algorithm, $hash . mt_rand(), true);
$result .= $hash[mt_rand(0, strlen($hash) - 1)];
}
return $result;
}
/**
* {@inheritdoc}
*/
public function isCryptographicallySecure()
{
return false;
}
}
+3 -11
View File
@@ -1,18 +1,10 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth\Providers\Rng;
interface IRNGProvider
{
/**
* @param int $bytecount the number of bytes of randomness to return
*
* @return string the random bytes
*/
public function getRandomBytes($bytecount);
/**
* @return bool whether this provider is cryptographically secure
*/
public function isCryptographicallySecure();
public function getRandomBytes(int $bytecount): string;
}
-37
View File
@@ -1,37 +0,0 @@
<?php
namespace RobThree\Auth\Providers\Rng;
class MCryptRNGProvider implements IRNGProvider
{
/** @var int */
private $source;
/**
* @param int $source
*/
public function __construct($source = MCRYPT_DEV_URANDOM)
{
$this->source = $source;
}
/**
* {@inheritdoc}
*/
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;
}
/**
* {@inheritdoc}
*/
public function isCryptographicallySecure()
{
return true;
}
}
-40
View File
@@ -1,40 +0,0 @@
<?php
namespace RobThree\Auth\Providers\Rng;
class OpenSSLRNGProvider implements IRNGProvider
{
/** @var bool */
private $requirestrong;
/**
* @param bool $requirestrong
*/
public function __construct($requirestrong = true)
{
$this->requirestrong = $requirestrong;
}
/**
* {@inheritdoc}
*/
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 isCryptographicallySecure()
{
return $this->requirestrong;
}
}
+5 -1
View File
@@ -1,7 +1,11 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth\Providers\Rng;
use RobThree\Auth\TwoFactorAuthException;
class RNGException extends TwoFactorAuthException {}
class RNGException extends TwoFactorAuthException
{
}
+17 -26
View File
@@ -1,34 +1,27 @@
<?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 string */
public $url;
/** @var string */
public $expectedtimeformat;
/** @var array */
public $options;
/**
* @param string $url
* @param string $expectedtimeformat
* @param array $options
* @param array<string, mixed> $options
*/
public function __construct($url = 'https://google.com', $expectedtimeformat = 'D, d M Y H:i:s O+', array $options = null)
{
$this->url = $url;
$this->expectedtimeformat = $expectedtimeformat;
if ($options === null) {
$options = array(
public function __construct(
public string $url = 'https://google.com',
public string $expectedtimeformat = 'D, d M Y H:i:s O+',
public ?array $options = null,
) {
if ($this->options === null) {
$this->options = array(
'http' => array(
'method' => 'HEAD',
'follow_location' => false,
@@ -38,12 +31,11 @@ class HttpTimeProvider implements ITimeProvider
'header' => array(
'Connection: close',
'User-agent: TwoFactorAuth HttpTimeProvider (https://github.com/RobThree/TwoFactorAuth)',
'Cache-Control: no-cache'
)
)
'Cache-Control: no-cache',
),
),
);
}
$this->options = $options;
}
/**
@@ -52,7 +44,7 @@ class HttpTimeProvider implements ITimeProvider
public function getTime()
{
try {
$context = stream_context_create($this->options);
$context = stream_context_create($this->options);
$fd = fopen($this->url, 'rb', false, $context);
$headers = stream_get_meta_data($fd);
fclose($fd);
@@ -62,10 +54,9 @@ class HttpTimeProvider implements ITimeProvider
return DateTime::createFromFormat($this->expectedtimeformat, trim(substr($h, 5)))->getTimestamp();
}
}
throw new \Exception('Invalid or no "Date:" header found');
} catch (\Exception $ex) {
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()));
}
}
}
+2
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth\Providers\Time;
interface ITimeProvider
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth\Providers\Time;
class LocalMachineTimeProvider implements ITimeProvider
+18 -30
View File
@@ -1,39 +1,27 @@
<?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
{
/** @var string */
public $host;
/** @var int */
public $port;
/** @var int */
public $timeout;
/**
* @param string $host
* @param int $port
* @param int $timeout
*/
public function __construct($host = 'time.google.com', $port = 123, $timeout = 1)
public function __construct(public string $host = 'time.google.com', public int $port = 123, public int $timeout = 1)
{
$this->host = $host;
if (!is_int($port) || $port <= 0 || $port > 65535) {
if ($this->port <= 0 || $this->port > 65535) {
throw new TimeException('Port must be 0 < port < 65535');
}
$this->port = $port;
if (!is_int($timeout) || $timeout < 0) {
if ($this->timeout < 0) {
throw new TimeException('Timeout must be >= 0');
}
$this->timeout = $timeout;
}
/**
@@ -42,28 +30,28 @@ class NTPTimeProvider implements ITimeProvider
public function getTime()
{
try {
/* Create a socket and connect to NTP server */
// 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, ['sec' => $this->timeout, 'usec' => 0]);
socket_set_option($sock, SOL_SOCKET, SO_RCVTIMEO, array('sec' => $this->timeout, 'usec' => 0));
socket_connect($sock, $this->host, $this->port);
/* Send request */
// Send request
$msg = "\010" . str_repeat("\0", 47);
socket_send($sock, $msg, strlen($msg), 0);
/* Receive response and close socket */
// Receive response and close socket
if (socket_recv($sock, $recv, 48, MSG_WAITALL) === false) {
throw new \Exception(socket_strerror(socket_last_error($sock)));
throw new Exception(socket_strerror(socket_last_error($sock)));
}
socket_close($sock);
/* Interpret response */
// Interpret response
$data = unpack('N12', $recv);
$timestamp = (int) sprintf('%u', $data[9]);
$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 */
// 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) {
} catch (Exception $ex) {
throw new TimeException(sprintf('Unable to retrieve time from %s (%s)', $this->host, $ex->getMessage()));
}
}
+5 -1
View File
@@ -1,7 +1,11 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth\Providers\Time;
use RobThree\Auth\TwoFactorAuthException;
class TimeException extends TwoFactorAuthException {}
class TimeException extends TwoFactorAuthException
{
}
+67 -216
View File
@@ -1,86 +1,48 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth;
use function hash_equals;
use RobThree\Auth\Providers\Qr\IQRCodeProvider;
use RobThree\Auth\Providers\Qr\QRServerProvider;
use RobThree\Auth\Providers\Rng\CSRNGProvider;
use RobThree\Auth\Providers\Rng\HashRNGProvider;
use RobThree\Auth\Providers\Rng\IRNGProvider;
use RobThree\Auth\Providers\Rng\MCryptRNGProvider;
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;
use SensitiveParameter;
// 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
{
/** @var string */
private $algorithm;
private static string $_base32dict = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567=';
/** @var int */
private $period;
/** @var array<string> */
private static array $_base32;
/** @var int */
private $digits;
/** @var array<string, int> */
private static array $_base32lookup = array();
/** @var string */
private $issuer;
/** @var ?IQRCodeProvider */
private $qrcodeprovider = null;
/** @var ?IRNGProvider */
private $rngprovider = null;
/** @var ?ITimeProvider */
private $timeprovider = null;
/** @var string */
private static $_base32dict = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567=';
/** @var array */
private static $_base32;
/** @var array */
private static $_base32lookup = array();
/** @var array */
private static $_supportedalgos = array('sha1', 'sha256', 'sha512', 'md5');
/**
* @param ?string $issuer
* @param int $digits
* @param int $period
* @param string $algorithm
* @param ?IQRCodeProvider $qrcodeprovider
* @param ?IRNGProvider $rngprovider
* @param ?ITimeProvider $timeprovider
*/
public function __construct($issuer = null, $digits = 6, $period = 30, $algorithm = 'sha1', IQRCodeProvider $qrcodeprovider = null, IRNGProvider $rngprovider = null, ITimeProvider $timeprovider = null)
{
$this->issuer = $issuer;
if (!is_int($digits) || $digits <= 0) {
throw new TwoFactorAuthException('Digits must be int > 0');
public function __construct(
private IQRCodeProvider $qrcodeprovider,
private readonly ?string $issuer = null,
private readonly int $digits = 6,
private readonly int $period = 30,
private readonly Algorithm $algorithm = Algorithm::Sha1,
private ?IRNGProvider $rngprovider = null,
private ?ITimeProvider $timeprovider = null
) {
if ($this->digits <= 0) {
throw new TwoFactorAuthException('Digits must be > 0');
}
$this->digits = $digits;
if (!is_int($period) || $period <= 0) {
if ($this->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;
$this->qrcodeprovider = $qrcodeprovider;
$this->rngprovider = $rngprovider;
$this->timeprovider = $timeprovider;
self::$_base32 = str_split(self::$_base32dict);
self::$_base32lookup = array_flip(self::$_base32);
@@ -88,20 +50,12 @@ class TwoFactorAuth
/**
* Create a new secret
*
* @param int $bits
* @param bool $requirecryptosecure
*
* @return string
*/
public function createSecret($bits = 80, $requirecryptosecure = true)
public function createSecret(int $bits = 160): string
{
$secret = '';
$bytes = (int) ceil($bits / 5); //We use 5 bits of each byte (since we have a 32-character 'alphabet' / BASE32)
$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 = $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
@@ -111,37 +65,24 @@ class TwoFactorAuth
/**
* Calculate the code with given secret and point in time
*
* @param string $secret
* @param ?int $time
*
* @return string
*/
public function getCode($secret, $time = null)
public function getCode(#[SensitiveParameter] 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((string) ($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
*
* @param string $secret
* @param string $code
* @param int $discrepancy
* @param ?int $time
* @param int $timeslice
*
* @return bool
*/
public function verifyCode($secret, $code, $discrepancy = 1, $time = null, &$timeslice = 0)
public function verifyCode(string $secret, string $code, int $discrepancy = 1, ?int $time = null, ?int &$timeslice = 0): bool
{
$timestamp = $this->getTime($time);
@@ -154,72 +95,38 @@ class TwoFactorAuth
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;
$timeslice = hash_equals($this->getCode($secret, $ts), $code) ? $slice : $timeslice;
}
return $timeslice > 0;
}
/**
* Timing-attack safe comparison of 2 codes (see http://blog.ircmaxell.com/2014/11/its-all-about-time.html)
*
* @param string $safe
* @param string $user
*
* @return bool
*/
private function codeEquals($safe, $user)
{
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;
}
/**
* Get data-uri of QRCode
*
* @param string $label
* @param string $secret
* @param mixed $size
*
* @return string
*/
public function getQRCodeImageAsDataUri($label, $secret, $size = 200)
public function getQRCodeImageAsDataUri(string $label, #[SensitiveParameter] 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:'
. $qrcodeprovider->getMimeType()
. $this->qrcodeprovider->getMimeType()
. ';base64,'
. base64_encode($qrcodeprovider->getQRCodeImage($this->getQRText($label, $secret), $size));
. base64_encode($this->qrcodeprovider->getQRCodeImage($this->getQRText($label, $secret), $size));
}
/**
* Compare default timeprovider with specified timeproviders and ensure the time is within the specified number of seconds (leniency)
* @param ?array $timeproviders
* @param int $leniency
*
* @return void
* @param array<ITimeProvider> $timeproviders
* @throws TwoFactorAuthException
*/
public function ensureCorrectTime(array $timeproviders = null, $leniency = 5)
public function ensureCorrectTime(?array $timeproviders = null, int $leniency = 5): void
{
if ($timeproviders === null) {
$timeproviders = array(
new NTPTimeProvider(),
new HttpTimeProvider()
new HttpTimeProvider(),
);
}
@@ -239,56 +146,50 @@ class TwoFactorAuth
}
}
/**
* @param ?int $time
*
* @return int
*/
private function getTime($time = null)
{
return ($time === null) ? $this->getTimeProvider()->getTime() : $time;
}
/**
* @param int $time
* @param int $offset
*
* @return int
*/
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
*
* @param string $label
* @param string $secret
*
* @return string
*/
public function getQRText($label, $secret)
public function getQRText(string $label, #[SensitiveParameter] string $secret): string
{
return 'otpauth://totp/' . rawurlencode($label)
. '?secret=' . rawurlencode($secret)
. '&issuer=' . rawurlencode((string)$this->issuer)
. '&period=' . intval($this->period)
. '&algorithm=' . rawurlencode(strtoupper($this->algorithm))
. '&digits=' . intval($this->digits);
. '&period=' . $this->period
. '&algorithm=' . rawurlencode(strtoupper($this->algorithm->value))
. '&digits=' . $this->digits;
}
/**
* @param string $value
* @return string
* @throws TwoFactorAuthException
*/
private function base32Decode($value)
public function getRngProvider(): IRNGProvider
{
if (strlen($value) == 0) {
return $this->rngprovider ??= new CSRNGProvider();
}
public function getTimeProvider(): ITimeProvider
{
// Set default time provider if none was specified
return $this->timeprovider ??= new LocalMachineTimeProvider();
}
private function getTime(?int $time = null): int
{
return $time ?? $this->getTimeProvider()->getTime();
}
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 ($value === '') {
return '';
}
if (preg_match('/[^' . preg_quote(self::$_base32dict) . ']/', $value) !== 0) {
if (preg_match('/[^' . preg_quote(self::$_base32dict, '/') . ']/', $value) !== 0) {
throw new TwoFactorAuthException('Invalid base32 string');
}
@@ -307,54 +208,4 @@ class TwoFactorAuth
}
return $output;
}
/**
* @return IQRCodeProvider
* @throws TwoFactorAuthException
*/
public function getQrCodeProvider()
{
// Set default QR Code provider if none was specified
if (null === $this->qrcodeprovider) {
return $this->qrcodeprovider = new QRServerProvider();
}
return $this->qrcodeprovider;
}
/**
* @return IRNGProvider
* @throws TwoFactorAuthException
*/
public function getRngProvider()
{
if (null !== $this->rngprovider) {
return $this->rngprovider;
}
if (function_exists('random_bytes')) {
return $this->rngprovider = new CSRNGProvider();
}
if (function_exists('mcrypt_create_iv')) {
return $this->rngprovider = new MCryptRNGProvider();
}
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');
}
/**
* @return ITimeProvider
* @throws TwoFactorAuthException
*/
public function getTimeProvider()
{
// Set default time provider if none was specified
if (null === $this->timeprovider) {
return $this->timeprovider = new LocalMachineTimeProvider();
}
return $this->timeprovider;
}
}
+5 -1
View File
@@ -1,7 +1,11 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth;
use Exception;
class TwoFactorAuthException extends Exception {}
class TwoFactorAuthException extends Exception
{
}
+12
View File
@@ -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
-26
View File
@@ -1,26 +0,0 @@
<?php
namespace Tests;
trait MightNotMakeAssertions
{
/**
* This is a shim to support PHPUnit for php 5.6 and 7.0.
*
* It has to be named something that doesn't collide with existing
* TestCase methods as we can't support PHP return types right now
*
* @return void
*/
public function noAssertionsMade()
{
foreach (class_parents($this) as $parent) {
if (method_exists($parent, 'expectNotToPerformAssertions')) {
parent::expectNotToPerformAssertions();
return;
}
}
$this->assertTrue(true);
}
}
+24 -28
View File
@@ -1,57 +1,53 @@
<?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\Providers\Qr\IQRCodeProvider;
use RobThree\Auth\TwoFactorAuth;
use RobThree\Auth\TwoFactorAuthException;
use RobThree\Auth\Providers\Qr\HandlesDataUri;
class IQRCodeProviderTest extends TestCase
{
use HandlesDataUri;
/**
* @return void
*/
public function testTotpUriIsCorrect()
{
$qr = new TestQrProvider();
protected IQRCodeProvider $qr;
$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']);
protected function setUp(): void
{
$this->qr = new TestQrProvider();
}
/**
* @return void
*/
public function testTotpUriIsCorrectNoIssuer()
public function testTotpUriIsCorrect(): void
{
$qr = new TestQrProvider();
$tfa = new TwoFactorAuth($this->qr, 'Test&Issuer', 6, 30, Algorithm::Sha1);
$data = $this->DecodeDataUri($tfa->getQRCodeImageAsDataUri('Test&Label', 'VMR466AB62ZBOKHE'));
$this->assertSame('test/test', $data['mimetype']);
$this->assertSame('base64', $data['encoding']);
$this->assertSame('otpauth://totp/Test%26Label?secret=VMR466AB62ZBOKHE&issuer=Test%26Issuer&period=30&algorithm=SHA1&digits=6@200', $data['data']);
}
public function testTotpUriIsCorrectNoIssuer(): void
{
/**
* 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, 'sha1', $qr);
$tfa = new TwoFactorAuth($this->qr, null, 6, 30, Algorithm::Sha1);
$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']);
$this->assertSame('test/test', $data['mimetype']);
$this->assertSame('base64', $data['encoding']);
$this->assertSame('otpauth://totp/Test%26Label?secret=VMR466AB62ZBOKHE&issuer=&period=30&algorithm=SHA1&digits=6@200', $data['data']);
}
/**
* @return void
*/
public function testGetQRCodeImageAsDataUriThrowsOnInvalidSize()
public function testGetQRCodeImageAsDataUriThrowsOnInvalidSize(): void
{
$qr = new TestQrProvider();
$tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', $qr);
$tfa = new TwoFactorAuth($this->qr, 'Test', 6, 30, Algorithm::Sha1);
$this->expectException(TwoFactorAuthException::class);
+5 -9
View File
@@ -1,23 +1,19 @@
<?php
declare(strict_types=1);
namespace Tests\Providers\Qr;
use RobThree\Auth\Providers\Qr\IQRCodeProvider;
class TestQrProvider implements IQRCodeProvider
{
/**
* {@inheritdoc}
*/
public function getQRCodeImage($qrtext, $size)
public function getQRCodeImage(string $qrText, int $size): string
{
return $qrtext . '@' . $size;
return $qrText . '@' . $size;
}
/**
* {@inheritdoc}
*/
public function getMimeType()
public function getMimeType(): string
{
return 'test/test';
}
+7 -16
View File
@@ -1,30 +1,21 @@
<?php
declare(strict_types=1);
namespace Tests\Providers\Rng;
use PHPUnit\Framework\TestCase;
use Tests\MightNotMakeAssertions;
use RobThree\Auth\Providers\Rng\CSRNGProvider;
class CSRNGProviderTest extends TestCase
{
use NeedsRngLengths, MightNotMakeAssertions;
use NeedsRngLengths;
/**
* @requires function random_bytes
*
* @return void
*/
public function testCSRNGProvidersReturnExpectedNumberOfBytes()
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->noAssertionsMade();
$rng = new CSRNGProvider();
foreach ($this->rngTestLengths as $l) {
$this->assertSame($l, strlen($rng->getRandomBytes($l)));
}
}
}
@@ -1,24 +0,0 @@
<?php
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());
}
}
+7 -49
View File
@@ -1,61 +1,19 @@
<?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;
use Tests\Providers\Qr\TestQrProvider;
class IRNGProviderTest extends TestCase
{
/**
* @return void
*/
public function testCreateSecretThrowsOnInsecureRNGProvider()
public function testCreateSecret(): void
{
$rng = new TestRNGProvider();
$tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, $rng);
$this->expectException(TwoFactorAuthException::class);
$tfa->createSecret();
}
/**
* @return void
*/
public function testCreateSecretOverrideSecureDoesNotThrowOnInsecureRNG()
{
$rng = new TestRNGProvider();
$tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, $rng);
$this->assertEquals('ABCDEFGHIJKLMNOP', $tfa->createSecret(80, false));
}
/**
* @return void
*/
public function testCreateSecretDoesNotThrowOnSecureRNGProvider()
{
$rng = new TestRNGProvider(true);
$tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, $rng);
$this->assertEquals('ABCDEFGHIJKLMNOP', $tfa->createSecret());
}
/**
* @return void
*/
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));
$tfa = new TwoFactorAuth(new TestQrProvider(), 'Test', 6, 30, Algorithm::Sha1, null, null);
$this->assertIsString($tfa->createSecret());
}
}
@@ -1,32 +0,0 @@
<?php
namespace Tests\Providers\Rng;
use PHPUnit\Framework\TestCase;
use Tests\MightNotMakeAssertions;
use RobThree\Auth\Providers\Rng\MCryptRNGProvider;
class MCryptRNGProviderTest extends TestCase
{
use NeedsRngLengths, MightNotMakeAssertions;
/**
* @requires function mcrypt_create_iv
*
* @return void
*/
public function testMCryptRNGProvidersReturnExpectedNumberOfBytes()
{
if (function_exists('mcrypt_create_iv')) {
$rng = new MCryptRNGProvider();
foreach ($this->rngTestLengths as $l) {
$this->assertEquals($l, strlen($rng->getRandomBytes($l)));
}
$this->assertTrue($rng->isCryptographicallySecure());
} else {
$this->noAssertionsMade();
}
}
}
+3 -1
View File
@@ -1,9 +1,11 @@
<?php
declare(strict_types=1);
namespace Tests\Providers\Rng;
trait NeedsRngLengths
{
/** @var array */
/** @var array<int> */
protected $rngTestLengths = array(1, 16, 32, 256);
}
@@ -1,37 +0,0 @@
<?php
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());
}
}
-41
View File
@@ -1,41 +0,0 @@
<?php
namespace Tests\Providers\Rng;
use RobThree\Auth\Providers\Rng\IRNGProvider;
class TestRNGProvider implements IRNGProvider
{
/** @var bool */
private $isSecure;
/**
* @param bool $isSecure whether this provider is cryptographically secure
*/
function __construct($isSecure = false)
{
$this->isSecure = $isSecure;
}
/**
* {@inheritdoc}
*/
public function getRandomBytes($bytecount)
{
$result = '';
for ($i = 0; $i < $bytecount; $i++) {
$result .= chr($i);
}
return $result;
}
/**
* {@inheritdoc}
*/
public function isCryptographicallySecure()
{
return $this->isSecure;
}
}
+13 -23
View File
@@ -1,53 +1,43 @@
<?php
declare(strict_types=1);
namespace Tests\Providers\Time;
use PHPUnit\Framework\TestCase;
use Tests\MightNotMakeAssertions;
use RobThree\Auth\TwoFactorAuthException;
use RobThree\Auth\Algorithm;
use RobThree\Auth\TwoFactorAuth;
use RobThree\Auth\TwoFactorAuthException;
use Tests\Providers\Qr\TestQrProvider;
class ITimeProviderTest extends TestCase
{
use MightNotMakeAssertions;
/**
* @return void
*/
public function testEnsureCorrectTimeDoesNotThrowForCorrectTime()
public function testEnsureCorrectTimeDoesNotThrowForCorrectTime(): void
{
$this->expectNotToPerformAssertions();
$tpr1 = new TestTimeProvider(123);
$tpr2 = new TestTimeProvider(128);
$tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, null, $tpr1);
$tfa = new TwoFactorAuth(new TestQrProvider(), 'Test', 6, 30, Algorithm::Sha1, null, $tpr1);
$tfa->ensureCorrectTime(array($tpr2)); // 128 - 123 = 5 => within default leniency
$this->noAssertionsMade();
}
/**
* @return void
*/
public function testEnsureCorrectTimeThrowsOnIncorrectTime()
public function testEnsureCorrectTimeThrowsOnIncorrectTime(): void
{
$tpr1 = new TestTimeProvider(123);
$tpr2 = new TestTimeProvider(124);
$tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, null, $tpr1);
$tfa = new TwoFactorAuth(new TestQrProvider(), 'Test', 6, 30, Algorithm::Sha1, null, $tpr1);
$this->expectException(TwoFactorAuthException::class);
$tfa->ensureCorrectTime(array($tpr2), 0); // We force a leniency of 0, 124-123 = 1 so this should throw
}
/**
* @return void
*/
public function testEnsureDefaultTimeProviderReturnsCorrectTime()
public function testEnsureDefaultTimeProviderReturnsCorrectTime(): void
{
$tfa = new TwoFactorAuth('Test', 6, 30, 'sha1');
$this->expectNotToPerformAssertions();
$tfa = new TwoFactorAuth(new TestQrProvider(), 'Test', 6, 30, Algorithm::Sha1);
$tfa->ensureCorrectTime(array(new TestTimeProvider(time())), 1); // Use a leniency of 1, should the time change between both time() calls
$this->noAssertionsMade();
}
}
+3 -1
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Tests\Providers\Time;
use RobThree\Auth\Providers\Time\ITimeProvider;
@@ -12,7 +14,7 @@ class TestTimeProvider implements ITimeProvider
/**
* @param int $time
*/
function __construct($time)
public function __construct($time)
{
$this->time = $time;
}
+92 -138
View File
@@ -1,150 +1,118 @@
<?php
declare(strict_types=1);
namespace Tests;
use PHPUnit\Framework\TestCase;
use RobThree\Auth\TwoFactorAuthException;
use ReflectionMethod;
use RobThree\Auth\Algorithm;
use RobThree\Auth\Providers\Time\HttpTimeProvider;
use RobThree\Auth\Providers\Time\NTPTimeProvider;
use RobThree\Auth\TwoFactorAuth;
use RobThree\Auth\TwoFactorAuthException;
use Tests\Providers\Qr\TestQrProvider;
class TwoFactorAuthTest extends TestCase
{
use MightNotMakeAssertions;
/**
* @return void
*/
public function testConstructorThrowsOnInvalidDigits()
public function testConstructorThrowsOnInvalidDigits(): void
{
$this->expectException(TwoFactorAuthException::class);
new TwoFactorAuth('Test', 0);
new TwoFactorAuth(new TestQrProvider(), 'Test', 0);
}
/**
* @return void
*/
public function testConstructorThrowsOnInvalidPeriod()
public function testConstructorThrowsOnInvalidPeriod(): void
{
$this->expectException(TwoFactorAuthException::class);
new TwoFactorAuth('Test', 6, 0);
new TwoFactorAuth(new TestQrProvider(), 'Test', 6, 0);
}
/**
* @return void
*/
public function testConstructorThrowsOnInvalidAlgorithm()
public function testGetCodeReturnsCorrectResults(): void
{
$this->expectException(TwoFactorAuthException::class);
new TwoFactorAuth('Test', 6, 30, 'xxx');
$tfa = new TwoFactorAuth(new TestQrProvider(), 'Test');
$this->assertSame('543160', $tfa->getCode('VMR466AB62ZBOKHE', 1426847216));
$this->assertSame('538532', $tfa->getCode('VMR466AB62ZBOKHE', 0));
}
/**
* @return void
*/
public function testGetCodeReturnsCorrectResults()
public function testEnsureAllTimeProvidersReturnCorrectTime(): void
{
$tfa = new TwoFactorAuth('Test');
$this->assertEquals('543160', $tfa->getCode('VMR466AB62ZBOKHE', 1426847216));
$this->assertEquals('538532', $tfa->getCode('VMR466AB62ZBOKHE', 0));
}
/**
* @return void
*/
public function testEnsureAllTimeProvidersReturnCorrectTime()
{
$tfa = new TwoFactorAuth('Test', 6, 30, 'sha1');
$tfa = new TwoFactorAuth(new TestQrProvider(), 'Test', 6, 30, Algorithm::Sha1);
$tfa->ensureCorrectTime(array(
new \RobThree\Auth\Providers\Time\NTPTimeProvider(), // Uses pool.ntp.org by default
new NTPTimeProvider(), // Uses pool.ntp.org by default
//new \RobThree\Auth\Providers\Time\NTPTimeProvider('time.google.com'), // Somehow time.google.com and time.windows.com make travis timeout??
new \RobThree\Auth\Providers\Time\HttpTimeProvider(), // Uses google.com by default
new 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'),
new HttpTimeProvider('https://yahoo.com'),
));
$this->noAssertionsMade();
$this->expectNotToPerformAssertions();
}
/**
* @return void
*/
public function testVerifyCodeWorksCorrectly()
public function testVerifyCodeWorksCorrectly(): void
{
$tfa = new TwoFactorAuth('Test', 6, 30);
$tfa = new TwoFactorAuth(new TestQrProvider(), 'Test', 6, 30);
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847190));
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 0, 1426847190 + 29)); //Test discrepancy
$this->assertFalse($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 0, 1426847190 + 30)); //Test discrepancy
$this->assertFalse($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 0, 1426847190 - 1)); //Test discrepancy
$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->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->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205)); //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->assertFalse($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->assertFalse($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 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
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 2, 1426847205 + 65)); //Test discrepancy
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 2, 1426847205 - 65)); //Test discrepancy
}
/**
* @return void
*/
public function testVerifyCorrectTimeSliceIsReturned()
public function testVerifyCorrectTimeSliceIsReturned(): void
{
$tfa = new TwoFactorAuth('Test', 6, 30);
$tfa = new TwoFactorAuth(new TestQrProvider(), 'Test', 6, 30);
// We test with discrepancy 3 (so total of 7 codes: c-3, c-2, c-1, c, c+1, c+2, c+3
// Ensure each corresponding timeslice is returned correctly
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '534113', 3, 1426847190, $timeslice1));
$this->assertEquals(47561570, $timeslice1);
$this->assertSame(47561570, $timeslice1);
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '819652', 3, 1426847190, $timeslice2));
$this->assertEquals(47561571, $timeslice2);
$this->assertSame(47561571, $timeslice2);
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '915954', 3, 1426847190, $timeslice3));
$this->assertEquals(47561572, $timeslice3);
$this->assertSame(47561572, $timeslice3);
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 3, 1426847190, $timeslice4));
$this->assertEquals(47561573, $timeslice4);
$this->assertSame(47561573, $timeslice4);
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '348401', 3, 1426847190, $timeslice5));
$this->assertEquals(47561574, $timeslice5);
$this->assertSame(47561574, $timeslice5);
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '648525', 3, 1426847190, $timeslice6));
$this->assertEquals(47561575, $timeslice6);
$this->assertSame(47561575, $timeslice6);
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '170645', 3, 1426847190, $timeslice7));
$this->assertEquals(47561576, $timeslice7);
$this->assertSame(47561576, $timeslice7);
// Incorrect code should return false and a 0 timeslice
$this->assertFalse($tfa->verifyCode('VMR466AB62ZBOKHE', '111111', 3, 1426847190, $timeslice8));
$this->assertEquals(0, $timeslice8);
$this->assertSame(0, $timeslice8);
}
/**
* @return void
*/
public function testGetCodeThrowsOnInvalidBase32String1()
public function testGetCodeThrowsOnInvalidBase32String1(): void
{
$tfa = new TwoFactorAuth('Test');
$tfa = new TwoFactorAuth(new TestQrProvider(), 'Test');
$this->expectException(TwoFactorAuthException::class);
$tfa->getCode('FOO1BAR8BAZ9'); //1, 8 & 9 are invalid chars
}
/**
* @return void
*/
public function testGetCodeThrowsOnInvalidBase32String2()
public function testGetCodeThrowsOnInvalidBase32String2(): void
{
$tfa = new TwoFactorAuth('Test');
$tfa = new TwoFactorAuth(new TestQrProvider(), 'Test');
$this->expectException(TwoFactorAuthException::class);
$tfa->getCode('mzxw6==='); //Lowercase
}
/**
* @return void
*/
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
@@ -157,90 +125,76 @@ class TwoFactorAuthTest extends TestCase
// "In general, you don't want to break any encapsulation for the sake of testing (or as Mom used to say, "don't
// expose your privates!"). Most of the time, you should be able to test a class by exercising its public methods."
// Dave Thomas and Andy Hunt -- "Pragmatic Unit Testing
$tfa = new TwoFactorAuth('Test');
$tfa = new TwoFactorAuth(new TestQrProvider(), 'Test');
$method = new \ReflectionMethod(TwoFactorAuth::class, 'base32Decode');
$method->setAccessible(true);
$method = new ReflectionMethod(TwoFactorAuth::class, 'base32Decode');
// Test vectors from: https://tools.ietf.org/html/rfc4648#page-12
$this->assertEquals('', $method->invoke($tfa, ''));
$this->assertEquals('f', $method->invoke($tfa, 'MY======'));
$this->assertEquals('fo', $method->invoke($tfa, 'MZXQ===='));
$this->assertEquals('foo', $method->invoke($tfa, 'MZXW6==='));
$this->assertEquals('foob', $method->invoke($tfa, 'MZXW6YQ='));
$this->assertEquals('fooba', $method->invoke($tfa, 'MZXW6YTB'));
$this->assertEquals('foobar', $method->invoke($tfa, 'MZXW6YTBOI======'));
$this->assertSame('', $method->invoke($tfa, ''));
$this->assertSame('f', $method->invoke($tfa, 'MY======'));
$this->assertSame('fo', $method->invoke($tfa, 'MZXQ===='));
$this->assertSame('foo', $method->invoke($tfa, 'MZXW6==='));
$this->assertSame('foob', $method->invoke($tfa, 'MZXW6YQ='));
$this->assertSame('fooba', $method->invoke($tfa, 'MZXW6YTB'));
$this->assertSame('foobar', $method->invoke($tfa, 'MZXW6YTBOI======'));
}
/**
* @return void
*/
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:
// "In some circumstances, the use of padding ("=") in base-encoded data is not required or used."
$tfa = new TwoFactorAuth('Test');
$tfa = new TwoFactorAuth(new TestQrProvider(), 'Test');
$method = new \ReflectionMethod(TwoFactorAuth::class, 'base32Decode');
$method->setAccessible(true);
$method = new ReflectionMethod(TwoFactorAuth::class, 'base32Decode');
// Test vectors from: https://tools.ietf.org/html/rfc4648#page-12
$this->assertEquals('', $method->invoke($tfa, ''));
$this->assertEquals('f', $method->invoke($tfa, 'MY'));
$this->assertEquals('fo', $method->invoke($tfa, 'MZXQ'));
$this->assertEquals('foo', $method->invoke($tfa, 'MZXW6'));
$this->assertEquals('foob', $method->invoke($tfa, 'MZXW6YQ'));
$this->assertEquals('fooba', $method->invoke($tfa, 'MZXW6YTB'));
$this->assertEquals('foobar', $method->invoke($tfa, 'MZXW6YTBOI'));
$this->assertSame('', $method->invoke($tfa, ''));
$this->assertSame('f', $method->invoke($tfa, 'MY'));
$this->assertSame('fo', $method->invoke($tfa, 'MZXQ'));
$this->assertSame('foo', $method->invoke($tfa, 'MZXW6'));
$this->assertSame('foob', $method->invoke($tfa, 'MZXW6YQ'));
$this->assertSame('fooba', $method->invoke($tfa, 'MZXW6YTB'));
$this->assertSame('foobar', $method->invoke($tfa, 'MZXW6YTBOI'));
}
/**
* @return void
*/
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');
$this->assertEquals('94287082', $tfa->getCode($secret, 59));
$this->assertEquals('07081804', $tfa->getCode($secret, 1111111109));
$this->assertEquals('14050471', $tfa->getCode($secret, 1111111111));
$this->assertEquals('89005924', $tfa->getCode($secret, 1234567890));
$this->assertEquals('69279037', $tfa->getCode($secret, 2000000000));
$this->assertEquals('65353130', $tfa->getCode($secret, 20000000000));
$tfa = new TwoFactorAuth(new TestQrProvider(), 'Test', 8, 30, Algorithm::Sha1);
$this->assertSame('94287082', $tfa->getCode($secret, 59));
$this->assertSame('07081804', $tfa->getCode($secret, 1111111109));
$this->assertSame('14050471', $tfa->getCode($secret, 1111111111));
$this->assertSame('89005924', $tfa->getCode($secret, 1234567890));
$this->assertSame('69279037', $tfa->getCode($secret, 2000000000));
$this->assertSame('65353130', $tfa->getCode($secret, 20000000000));
}
/**
* @return void
*/
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');
$this->assertEquals('46119246', $tfa->getCode($secret, 59));
$this->assertEquals('68084774', $tfa->getCode($secret, 1111111109));
$this->assertEquals('67062674', $tfa->getCode($secret, 1111111111));
$this->assertEquals('91819424', $tfa->getCode($secret, 1234567890));
$this->assertEquals('90698825', $tfa->getCode($secret, 2000000000));
$this->assertEquals('77737706', $tfa->getCode($secret, 20000000000));
$tfa = new TwoFactorAuth(new TestQrProvider(), 'Test', 8, 30, Algorithm::Sha256);
$this->assertSame('46119246', $tfa->getCode($secret, 59));
$this->assertSame('68084774', $tfa->getCode($secret, 1111111109));
$this->assertSame('67062674', $tfa->getCode($secret, 1111111111));
$this->assertSame('91819424', $tfa->getCode($secret, 1234567890));
$this->assertSame('90698825', $tfa->getCode($secret, 2000000000));
$this->assertSame('77737706', $tfa->getCode($secret, 20000000000));
}
/**
* @return void
*/
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');
$this->assertEquals('90693936', $tfa->getCode($secret, 59));
$this->assertEquals('25091201', $tfa->getCode($secret, 1111111109));
$this->assertEquals('99943326', $tfa->getCode($secret, 1111111111));
$this->assertEquals('93441116', $tfa->getCode($secret, 1234567890));
$this->assertEquals('38618901', $tfa->getCode($secret, 2000000000));
$this->assertEquals('47863826', $tfa->getCode($secret, 20000000000));
$tfa = new TwoFactorAuth(new TestQrProvider(), 'Test', 8, 30, Algorithm::Sha512);
$this->assertSame('90693936', $tfa->getCode($secret, 59));
$this->assertSame('25091201', $tfa->getCode($secret, 1111111109));
$this->assertSame('99943326', $tfa->getCode($secret, 1111111111));
$this->assertSame('93441116', $tfa->getCode($secret, 1234567890));
$this->assertSame('38618901', $tfa->getCode($secret, 2000000000));
$this->assertSame('47863826', $tfa->getCode($secret, 20000000000));
}
}
+27 -26
View File
@@ -1,61 +1,62 @@
<?php
declare(strict_types=1);
namespace TestsDependency;
use BaconQrCode\Renderer\Image\ImagickImageBackEnd;
use PHPUnit\Framework\TestCase;
use RobThree\Auth\Algorithm;
use RobThree\Auth\Providers\Qr\BaconQrCodeProvider;
use RobThree\Auth\TwoFactorAuth;
use RobThree\Auth\Providers\Qr\HandlesDataUri;
use RobThree\Auth\Providers\Qr\IQRCodeProvider;
use RobThree\Auth\TwoFactorAuth;
use RuntimeException;
class BaconQRCodeTest extends TestCase
{
use HandlesDataUri;
public function testDependency()
protected IQRCodeProvider $qr;
protected function setUp(): void
{
// php < 7.1 will install an older Bacon QR Code
if (! class_exists(ImagickImageBackEnd::class)) {
$this->expectException(\RuntimeException::class);
$qr = new BaconQrCodeProvider(1, '#000', '#FFF', 'svg');
} else {
$qr = new BaconQrCodeProvider(1, '#000', '#FFF', 'svg');
$tfa = new TwoFactorAuth('Test&Issuer', 6, 30, 'sha1', $qr);
$data = $this->DecodeDataUri($tfa->getQRCodeImageAsDataUri('Test&Label', 'VMR466AB62ZBOKHE'));
$this->assertEquals('image/svg+xml', $data['mimetype']);
}
$this->qr = new BaconQrCodeProvider(1, '#000', '#FFF', 'svg');
;
}
public function testBadTextColour()
public function testDependency(): void
{
$this->expectException(\RuntimeException::class);
$tfa = new TwoFactorAuth($this->qr, 'Test&Issuer', 6, 30, Algorithm::Sha1);
$data = $this->DecodeDataUri($tfa->getQRCodeImageAsDataUri('Test&Label', 'VMR466AB62ZBOKHE'));
$this->assertSame('image/svg+xml', $data['mimetype']);
}
public function testBadTextColour(): void
{
$this->expectException(RuntimeException::class);
new BaconQrCodeProvider(1, 'not-a-colour', '#FFF');
}
public function testBadBackgroundColour()
public function testBadBackgroundColour(): void
{
$this->expectException(\RuntimeException::class);
$this->expectException(RuntimeException::class);
new BaconQrCodeProvider(1, '#000', 'not-a-colour');
}
public function testBadTextColourHexRef()
public function testBadTextColourHexRef(): void
{
$this->expectException(\RuntimeException::class);
$this->expectException(RuntimeException::class);
new BaconQrCodeProvider(1, '#AAAA', '#FFF');
}
public function testBadBackgroundColourHexRef()
public function testBadBackgroundColourHexRef(): void
{
$this->expectException(\RuntimeException::class);
$this->expectException(RuntimeException::class);
new BaconQrCodeProvider(1, '#000', '#AAAA');
}
}
+8 -6
View File
@@ -1,24 +1,26 @@
<?php
declare(strict_types=1);
namespace TestsDependency;
use PHPUnit\Framework\TestCase;
use RobThree\Auth\TwoFactorAuth;
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()
public function testDependency(): void
{
$qr = new EndroidQrCodeProvider();
$tfa = new TwoFactorAuth('Test&Issuer', 6, 30, 'sha1', $qr);
$tfa = new TwoFactorAuth($qr, 'Test&Issuer', 6, 30, Algorithm::Sha1);
$data = $this->DecodeDataUri($tfa->getQRCodeImageAsDataUri('Test&Label', 'VMR466AB62ZBOKHE'));
$this->assertEquals('image/png', $data['mimetype']);
$this->assertEquals('base64', $data['encoding']);
$this->assertSame('image/png', $data['mimetype']);
$this->assertSame('base64', $data['encoding']);
$this->assertNotEmpty($data['data']);
}
}