Compare commits

...

252 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
William Hall 65681de5a3 🔖 actually 1.8.2 2022-03-22 16:11:07 +00:00
William Hall dc681e1ced Merge remote-tracking branch 'mpeveler/bugfix-endroid-4' 2022-02-14 17:14:18 +00:00
Matthew Peveler 5f2611cde2 Fix compatibility with endroid/qr-code 4 2022-02-14 11:09:10 -05:00
William Hall 5d36d4feb0 Merge remote-tracking branch 'origin/test-specific-provider' 2021-12-29 18:17:40 +00:00
William Hall cb45226800 🚨 test in php < 7.1 2021-12-19 19:50:25 +00:00
William Hall 2d1ec8d265 🐛 be more specific with hex colour handling 2021-12-19 19:44:10 +00:00
William Hall ae4da10ff1 🚨 test bacon qr code provider 2021-12-19 19:33:11 +00:00
William Hall 66e1e030ba ♻️ re arrange useful testing code for provider tests 2021-12-19 19:32:45 +00:00
Will Power e76f31e93b Merge pull request #83 from brainfoolong/master
convert issuer to string prevents php 8.1 errors
2021-12-19 18:21:51 +00:00
Will Power 4203749bd2 Merge pull request #85 from MasterOdin/mpeveler/chore-deprecations
Catch deprecations as part of test suite
2021-12-19 18:19:17 +00:00
William Hall 4f1543f782 🚨 add test to cover null issuer 2021-12-19 18:16:38 +00:00
Matthew Peveler 8a220fe37b Catch deprecations as part of test suite 2021-12-01 10:12:42 -05:00
William Hall 8a5cb24967 🔧 add 8.1 to testing to confirm issue 2021-12-01 09:34:38 +00:00
Roland Eigelsreiter dc9f168746 added 8.1 to builds 2021-11-29 10:25:23 +01:00
Roland Eigelsreiter 0096cce02d convert issuer to string prevents php 8.1 errors
By forcing $this->issuer to be string, even if null is set, it prevents throwing errors in PHP 8.1 is `rawurlencode` is not allowed to have null as parameter.

It would be better to force string to be already in `__construct`, but this may create a breaking change for existing users.
2021-11-29 06:50:54 +01:00
Rob Janssen 042f347666 Update LICENSE 2021-11-02 15:07:43 +01:00
Rob Janssen 5afcb45282 Update composer.json
Upped version
2021-10-20 14:19:55 +02:00
Rob Janssen b79d438032 Merge pull request #79 from fman42/ChartGoogleQrCodeProvider
Implemented new ChartGoogleQrCodeProvider
2021-10-20 14:17:52 +02:00
Milan 65b17bca59 Add encoding option in query to google API 2021-10-17 21:21:54 +03:00
Milan c01202bc10 Make verifySSL argument as first option in constructor 2021-10-17 21:08:32 +03:00
fman42 0453a94349 Made verifySSL optional 2021-10-04 15:01:04 +03:00
fman42 49f681e905 Rename a class 2021-10-04 15:00:06 +03:00
Milan 08846488cc Implemented new ChartGoogleQrCodeProvider 2021-10-03 19:14:10 +03:00
William Hall e15885ce9d Merge remote-tracking branch 'willpower232/master' 2021-03-29 18:07:39 +01:00
William Hall f006e63cff 📚 break down documentation for github pages 2021-03-29 18:05:08 +01:00
William Hall 1faaf71391 Merge branch 'clean-up-demo' 2021-03-29 18:02:58 +01:00
William Hall 148e409a38 Merge remote-tracking branch 'MasterOdin/patch-5' 2021-03-29 18:01:15 +01:00
William Hall a627b889af 📚 clean up demo 2021-03-19 17:05:13 +00:00
Matthew Peveler b24204bbed Remove using https://github.com as HttpTimeProvider test source
Signed-off-by: Matthew Peveler <matt.peveler@gmail.com>
2021-03-15 08:43:09 -04:00
William Hall 30a38627ae 🔖 1.8.0 2021-03-09 18:24:05 +00:00
William Hall b99f4f96d0 📚 document that we now test against PHP 8 2021-03-09 18:23:12 +00:00
William Hall 38f349fccd Merge remote-tracking branch 'igorsantos/qr-logos' 2021-03-09 18:09:38 +00:00
William Hall 3e9d402a6e Merge branch 'doc-blocks' 2021-03-09 18:00:40 +00:00
William Hall 33a32cb099 📚 doc blocks 2021-03-08 18:24:44 +00:00
William Hall 452d31bfbf Merge branch 'fix-code-sniffs' 2021-03-08 18:22:30 +00:00
William Hall 58d3354367 🐛 correct some minor issues 2021-03-08 18:20:07 +00:00
William Hall 2aa6f46e20 🎨 fix the majority of code sniffs 2021-02-08 17:28:05 +00:00
William Hall 14e90c3faa Merge branch 'fix-unthrowable-exception' 2021-02-08 17:20:43 +00:00
William Hall df74a59b43 🐛 fix unthrowable exception 2021-02-05 16:30:19 +00:00
Will Power 3640fee09a Merge pull request #59 from MasterOdin/github_action
migrate from travis to gh action
2021-02-04 14:56:00 +00:00
Matthew Peveler 2a2b91023c add php 8.0 to matrix
Signed-off-by: Matthew Peveler <matt.peveler@popsql.com>
2021-02-04 09:47:09 -05:00
Matthew Peveler bbf24892db update
Signed-off-by: Matthew Peveler <matt.peveler@popsql.com>
2021-02-04 09:45:11 -05:00
Matthew Peveler 9011515f87 Merge branch 'master' into github_action 2021-02-04 09:44:09 -05:00
William Hall 1b7197f1b3 Merge branch 'test-refactor' 2021-02-04 11:42:29 +00:00
Rob Janssen 2c2afdb49b Create FUNDING.yml 2021-02-04 02:17:27 +01:00
William Hall 8b5c61cc1d 🚨 add auto lint test capability 2021-02-03 09:31:27 +00:00
William Hall d86c73ebff 🚨 run tests easier and newer 2021-02-03 09:31:16 +00:00
William Hall 178f4447a7 🚨 breakdown tests into separate files 2021-02-03 09:31:07 +00:00
William Hall 5957248509 🗑️ remove composer.lock from project to unlock dependencies 2021-02-02 18:28:12 +00:00
Matthew Peveler 7d21cee5c1 migrate from travis to gh action
Signed-off-by: Matthew Peveler <matt.peveler@gmail.com>
2021-01-28 11:49:01 +00:00
Igor Santos cc2ae19bcd Document the need for the code to be a string 2021-01-25 02:32:41 -03:00
Igor Santos 94571a257a Remove temp file(?) 2021-01-24 00:41:46 -03:00
Igor Santos ee9bf04ab7 Mention embedded image support in docs 2021-01-24 00:41:46 -03:00
Igor Santos c3f3c0a849 Move logo implementation to its own class 2021-01-24 00:41:46 -03:00
Igor Santos 2060811d88 Add logo option to Endroid's provider
closes #56
2021-01-23 23:58:22 -03:00
Rob Janssen c3110d9760 Assert in non-asserting ('risky') tests 2020-11-02 16:36:58 +01:00
Rob Janssen 4efb71d0ba Update README.md 2020-11-02 15:17:00 +01:00
Rob Janssen b5f9eda499 Update README.md
Fixed links
2020-09-28 10:26:53 +02:00
Rob Janssen 4678ce91a2 Merge pull request #51 from MasterOdin/patch-2
Update README with two new QR providers
2020-09-28 10:24:44 +02:00
Matthew Peveler 2daae60a34 Update README with two new QR providers
Signed-off-by: Matthew Peveler <matt.peveler@gmail.com>
2020-09-28 01:26:27 +00:00
Rob Janssen 44c06ba856 Update composer.json
See #40 and #39
2020-09-27 23:04:50 +02:00
Rob Janssen 9007489436 Merge pull request #39 from willpower232/bacon-qr-code-integration
integrate bacon qr code
2020-09-27 23:03:10 +02:00
Rob Janssen f0136c9458 Merge pull request #40 from SavageCore/feature/endroidqrcode
Implement `endroid/qr-code` support
2020-09-27 23:01:52 +02:00
Rob Janssen de411a22eb Merge pull request #50 from MasterOdin/patch-2
Mark QRServerProvider as default in README
2020-09-27 22:55:17 +02:00
Matthew Peveler 7c0f0b3d7f Mark QRServerProvider as default in README
Signed-off-by: Matthew Peveler <matt.peveler@gmail.com>
2020-09-27 10:04:56 -04:00
Rob Janssen 4da0739b38 Fix typo in README 2020-05-07 18:27:40 +02:00
Rob Janssen 37983bf675 Dropping support for older PHP versions (5.4, 5.5) 2020-01-02 20:56:46 +01:00
RobThree 1a941f0c86 Dropping support for older PHP versions (5.4, 5.5) 2020-01-02 20:54:30 +01:00
Rob Janssen c6f1f47481 Update NTPTimeProvider.php 2020-01-02 20:47:51 +01:00
William Hall 45bdc41597 integrate bacon qr code 2019-12-01 10:55:08 +00:00
SavageCore 957128bcfe Implement endroid/qr-code support 2019-11-20 17:36:27 +00:00
RobThree 3407c33775 * Switched default ImageChartsQRCodeProvider to QRServerProvider 2019-06-21 10:51:04 +02:00
Rob Janssen 401fc07652 Update README.md 2019-04-29 12:08:22 +02:00
Rob Janssen b5cd72a00a Update README.md 2019-04-29 12:05:24 +02:00
RobThree 7477d5d656 * Replace google image charts with image-charts.com 2019-03-19 22:51:19 +01:00
RobThree f5f58a4c62 Merge branch 'master' of https://github.com/RobThree/TwoFactorAuth 2018-06-09 12:09:59 +02:00
RobThree 718160e171 * Upped version 2018-06-09 12:09:51 +02:00
RobThree c18ec155ae * Minor refactoring of verifyCode. 2018-06-09 12:09:24 +02:00
Rob Janssen 75c49351a6 Update README.md 2018-06-06 17:52:17 +02:00
Rob Janssen e74f7a4bf6 Update README.md 2018-06-06 17:51:17 +02:00
RobThree fca87f2d09 * Fix a bug that caused timeslices to be returned instead of the intended timeslice
* Upped version
2018-06-06 02:32:07 +02:00
Rob Janssen 72572c5c3a * Removed hhvm badge 2018-06-06 01:59:46 +02:00
RobThree a813bf7ede * Upped version to 1.6.3 2018-06-06 01:58:26 +02:00
Rob Janssen 39db2654a6 Update README.md 2018-06-06 01:56:01 +02:00
RobThree 4ac2670429 * Try older PHP versions for the last time 2018-06-06 01:50:29 +02:00
RobThree 5de91c2837 * Ignore deprecation message for mcrypt_create_iv for PHP7.1+ 2018-06-06 01:49:21 +02:00
RobThree 0ac68f6b86 * Fix PHP7.2 issue with HashRNGProvider
* Fix PHP7.1+ issue with MCryptRNGProvider
2018-06-06 01:47:03 +02:00
RobThree fac4ebd44b * Remove time.google.com to see if that will make travis build again 2018-06-06 01:38:51 +02:00
RobThree ad8b7ab3e8 * Trying some build configurations 2018-06-06 01:35:56 +02:00
RobThree d7e8ad8e23 * Trying some build configurations 2018-06-06 01:32:25 +02:00
RobThree b8befc5aff * Put back conditional phpunit running 2018-06-06 01:15:37 +02:00
RobThree 37fbb99ac5 * Run phpunit for all versions 2018-06-06 00:58:30 +02:00
RobThree ea9b87fe32 * Changed time.windows.com to time.google.com (doesn't affect anything except that it should stop travis from failing) 2018-06-06 00:53:02 +02:00
RobThree 4f2364fef4 * Add whitelist to phpunit.xml 2018-06-05 23:05:08 +02:00
RobThree e4ec94e14d * Drop PHP 5.3 again
* Add phpunit.xml
2018-06-05 23:02:37 +02:00
RobThree de210192a7 * Put back PHP 5.3 support 2018-06-05 22:46:16 +02:00
RobThree f25b910be9 * Put back phpunit in travis.yml 2018-06-05 22:42:00 +02:00
RobThree b591c879a1 * Put back dist: trusty in travis.yml 2018-06-05 22:39:47 +02:00
RobThree 4408ce7884 * Dropped support for PHP 5.3
* Added support for PHP 7.2
2018-06-05 22:36:40 +02:00
RobThree 83ad9fb4e4 * Updated composer.json 2018-06-05 22:20:08 +02:00
RobThree 93fc6355d4 * Dropped ConvertUnixTimeDotComTimeProvider
* Added NTPTimeProvider
* Added out argument on verifyCode() method that returns the matched timeslice (if any) when a valid code was entered
2018-06-05 22:17:49 +02:00
Rob Janssen a77e7d8223 Update README.md 2017-11-06 18:55:56 +01:00
RobThree 673223a2fc * Upped version 2017-11-06 18:54:34 +01:00
RobThree d450ba432d * Attempt 2 to fix Travis 2017-11-06 18:50:34 +01:00
RobThree d22329375f * Attempt to fix Travis build 2017-11-06 18:48:32 +01:00
RobThree b1ecc9afbc * Fix some caching issues 2017-11-06 18:37:49 +01:00
Rob Janssen c9bfe0519a Undo previous commit 2017-02-18 15:43:20 +01:00
Rob Janssen 404f147aa7 Add PHP 7.1 and nightly 2017-02-18 15:37:04 +01:00
Rob Janssen 18888f83ee Delete applicationhost.config 2017-02-17 18:00:58 +01:00
RobThree 3d04717346 * Add .vs folder to .gitignore 2017-02-17 18:00:19 +01:00
72 changed files with 2012 additions and 3082 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!
+4
View File
@@ -0,0 +1,4 @@
# These are supported funding model platforms
github: [RobThree]
custom: ["https://paypal.me/robiii"]
+36
View File
@@ -0,0 +1,36 @@
name: Test Bacon QR Code Provider
on:
push:
pull_request:
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
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@v5
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
tools: composer
coverage: xdebug
ini-values: error_reporting=E_ALL
- uses: ramsey/composer-install@v3
- run: composer require bacon/bacon-qr-code:${{ matrix.bacon-version }}
- run: composer lint-ci
- run: composer test testsDependency/BaconQRCodeTest.php
+44
View File
@@ -0,0 +1,44 @@
name: Test Endroid QR Code Provider
on:
push:
pull_request:
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
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@v5
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
tools: composer
coverage: xdebug
ini-values: error_reporting=E_ALL
- uses: ramsey/composer-install@v3
- run: composer require endroid/qrcode:${{ matrix.endroid-version }} -W
- run: composer lint-ci
- run: composer test testsDependency/EndroidQRCodeTest.php
+29
View File
@@ -0,0 +1,29 @@
name: Test
on:
push:
pull_request:
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
php-version: [8.2, 8.3, 8.4, 8.5]
steps:
- uses: actions/checkout@v5
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
tools: composer
coverage: xdebug
ini-values: error_reporting=E_ALL
- uses: ramsey/composer-install@v3
- run: composer lint-ci
- run: composer phpstan
- run: composer test
+9 -2
View File
@@ -125,7 +125,7 @@ publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# TODO: Comment the next line if you want to checkin your web deploy settings
# TODO: Comment the next line if you want to checkin your web deploy settings
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
@@ -183,4 +183,11 @@ UpgradeLog*.htm
FakesAssemblies/
# Composer
/vendor
/vendor
composer.lock
# .vs
.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)
;
-11
View File
@@ -1,11 +0,0 @@
language: php
php:
- 5.3
- 5.4
- 5.5
- 5.6
- 7
- hhvm
script: phpunit --coverage-text tests
File diff suppressed because it is too large Load Diff
+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.
+1 -1
View File
@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2014-2015 Rob Janssen
Copyright (c) 2014-2021 Rob Janssen and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+15 -169
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/travis/RobThree/TwoFactorAuth.svg?style=flat-square)](https://travis-ci.org/RobThree/TwoFactorAuth/) [![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) [![HHVM Status](https://img.shields.io/hhvm/RobThree/TwoFactorAuth.svg?style=flat-square)](http://hhvm.h4cc.de/package/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,188 +10,34 @@ PHP library for [two-factor (or multi-factor) authentication](http://en.wikipedi
## Requirements
* Tested on PHP 5.3, 5.4, 5.5 and 5.6, 7 and HHVM
* [cURL](http://php.net/manual/en/book.curl.php) when using the provided `GoogleQRCodeProvider` (default), `QRServerProvider` or `QRicketProvider` but you can also provide your own QR-code provider.
* [random_bytes()](http://php.net/manual/en/function.random-bytes.php), [MCrypt](http://php.net/manual/en/book.mcrypt.php), [OpenSSL](http://php.net/manual/en/book.openssl.php) or [Hash](http://php.net/manual/en/book.hash.php) depending on which built-in RNG you use (TwoFactorAuth will try to 'autodetect' and use the best available); however: feel free to provide your own (CS)RNG.
* Requires PHP version >=8.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
Run the following command:
The best way of installing this library is with composer:
`php composer.phar require robthree/twofactorauth`
## Quick start
If you want to hit the ground running then have a look at the [demo](demo/demo.php). It's very simple and easy!
## Usage
Here are some code snippets that should help you get started...
For a quick start, have a look at the [getting started](https://robthree.github.io/TwoFactorAuth/getting-started.html) page or try out the [demo](demo/demo.php).
````php
// Create a TwoFactorAuth instance
$tfa = new RobThree\Auth\TwoFactorAuth('My Company');
````
The TwoFactorAuth class constructor accepts 7 parameters (all optional):
Parameter | Default value | Use
------------------|---------------|--------------------------------------------------
`$issuer` | `null` | Will be displayed in the app as issuer name
`$digits` | `6` | The number of digits the resulting codes will be
`$period` | `30` | The number of seconds a code will be valid
`$algorithm` | `sha1` | The algorithm used
`$qrcodeprovider` | `null` | QR-code provider (more on this later)
`$rngprovider` | `null` | Random Number Generator provider (more on this later)
`$timeprovider` | `null` | Time provider (more on this later)
These parameters are all '`write once`'; the class will, for it's lifetime, use these values when generating / calculating codes. The number of digits, the period and algorithm are all set to values Google's Authticator app uses (and supports). You may specify `8` digits, a period of `45` seconds and the `sha256` algorithm but the authenticator app (be it Google's implementation, Authy or any other app) may or may not support these values. Your mileage may vary; keep it on the safe side if you don't control which app your audience uses.
### Step 1: Set up secret shared key
When a user wants to setup two-factor auth (or, more correctly, multi-factor auth) you need to create a secret. This will be your **shared secret**. This secret will need to be entered by the user in their app. This can be done manually, in which case you simply display the secret and have the user type it in the app:
````php
$secret = $tfa->createSecret();
````
The `createSecret()` method accepts two arguments: `$bits` (default: `80`) and `$requirecryptosecure` (default: `true`). The former is the number of bits generated for the shared secret. Make sure this argument is a multiple of 8 and, again, keep in mind that not all combinations may be supported by all apps. Google authenticator seems happy with 80 and 160, the default is set to 80 because that's what most sites (that I know of) currently use; however a value of 160 or higher is recommended (see [RFC 4226 - Algorithm Requirements](https://tools.ietf.org/html/rfc4226#section-4)). The latter is used to ensure that the secret is cryptographically secure; if you don't care very much for cryptographically secure secrets you can specify `false` and use a **non**-cryptographically secure RNG provider.
````php
// Display shared secret
<p>Please enter the following code in your app: '<?php echo $secret; ?>'</p>
````
Another, more user-friendly, way to get the shared secret into the app is to generate a [QR-code](http://en.wikipedia.org/wiki/QR_code) which can be scanned by the app. To generate these QR codes you can use any one of the built-in `QRProvider` classes:
1. `GoogleQRCodeProvider` (default)
2. `QRServerProvider`
3. `QRicketProvider`
...or implement your own provider. To implement your own provider all you need to do is implement the `IQRCodeProvider` interface. You can use the built-in providers mentioned before to serve as an example or read the next chapter in this file. The built-in classes all use a 3rd (e.g. external) party (Google, QRServer and QRicket) for the hard work of generating QR-codes (note: each of these services might at some point not be available or impose limitations to the number of codes generated per day, hour etc.). You could, however, easily use a project like [PHP QR Code](http://phpqrcode.sourceforge.net/) (or one of the [many others](https://packagist.org/search/?q=qr)) to generate your QR-codes without depending on external sources. Later on we'll [demonstrate](#qr-code-providers) how to do this.
The built-in providers all have some provider-specific 'tweaks' you can 'apply'. Some provide support for different colors, others may let you specify the desired image-format etc. What they all have in common is that they return a QR-code as binary blob which, in turn, will be turned into a [data URI](http://en.wikipedia.org/wiki/Data_URI_scheme) by the `TwoFactorAuth` class. This makes it easy for you to display the image without requiring extra 'roundtrips' from browser to server and vice versa.
````php
// Display QR code to user
<p>Scan the following image with your app:</p>
<p><img src="<?php echo $tfa->getQRCodeImageAsDataUri('Bob Ross', $secret); ?>"></p>
````
When outputting a QR-code you can choose a `$label` for the user (which, when entering a shared secret manually, will have to be chosen by the user). This label may be an empty string or `null`. Also a `$size` may be specified (in pixels, width == height) for which we use a default value of `200`.
### Step 2: Verify secret shared key
When the shared secret is added to the app, the app will be ready to start generating codes which 'expire' each '`$period`' number of seconds. To make sure the secret was entered, or scanned, correctly you need to verify this by having the user enter a generated code. To check if the generated code is valid you call the `verifyCode()` method:
````php
// Verify code
$result = $tfa->verifyCode($_SESSION['secret'], $_POST['verification']);
````
`verifyCode()` will return either `true` (the code was valid) or `false` (the code was invalid; no points for you!). You may need to store `$secret` in a `$_SESSION` or other persistent storage between requests. The `verifyCode()` accepts, aside from `$secret` and `$code`, two more parameters. The first being `$discrepancy`. Since TOTP codes are based on time("slices") it is very important that the server (but also client) have a correct date/time. But because the two *may* differ a bit we usually allow a certain amount of leeway. Because generated codes are valid for a specific period (remember the `$period` parameter in the `TwoFactorAuth`'s constructor?) we usually check the period directly before and the period directly after the current time when validating codes. So when the current time is `14:34:21`, which results in a 'current timeslice' of `14:34:00` to `14:34:30` we also calculate/verify the codes for `14:33:30` to `14:34:00` and for `14:34:30` to `14:35:00`. This gives us a 'window' of `14:33:30` to `14:35:00`. The `$discrepancy` parameter specifies how many periods (or: timeslices) we check in either direction of the current time. The default `$discrepancy` of `1` results in (max.) 3 period checks: -1, current and +1 period. A `$discrepancy` of `4` would result in a larger window (or: bigger time difference between client and server) of -4, -3, -2, -1, current, +1, +2, +3 and +4 periods.
The second parameter `$time` allows you to check a code for a specific point in time. This parameter has no real practical use but can be handy for unittesting etc. The default value, `null`, means: use the current time.
### Step 3: Store `$secret` with user and we're done!
Ok, so now the code has been verified and found to be correct. Now we can store the `$secret` with our user in our database (or elsewhere) and whenever the user begins a new session we ask for a code generated by the authentication app of their choice. All we need to do is call `verifyCode()` again with the shared secret and the entered code and we know if the user is legit or not.
Simple as 1-2-3.
All we need is 3 methods and a constructor:
````php
public function __construct(
$issuer = null,
$digits = 6,
$period = 30,
$algorithm = 'sha1',
RobThree\Auth\Providers\Qr\IQRCodeProvider $qrcodeprovider = null,
RobThree\Auth\Providers\Rng\IRNGProvider $rngprovider = null
);
public function createSecret($bits = 80, $requirecryptosecure = true): string;
public function getQRCodeImageAsDataUri($label, $secret, $size = 200): string;
public function verifyCode($secret, $code, $discrepancy = 1, $time = null): bool;
````
### QR-code providers
As mentioned before, this library comes with three 'built-in' QR-code providers. This chapter will touch the subject a bit but most of it should be self-explanatory. The `TwoFactorAuth`-class accepts a `$qrcodeprovider` parameter which lets you specify a built-in or custom QR-code provider. All three built-in providers do a simple HTTP request to retrieve an image using cURL and implement the [`IQRCodeProvider`](lib/Providers/Qr/IQRCodeProvider.php) interface which is all you need to implement to write your own QR-code provider.
The default provider is the [`GoogleQRCodeProvider`](lib/Providers/Qr/GoogleQRCodeProvider.php) which uses the [Google Chart Tools](https://developers.google.com/chart/infographics/docs/qr_codes) to render QR-codes. Then we have the [`QRServerProvider`](lib/Providers/Qr/QRServerProvider.php) which uses the [goqr.me API](http://goqr.me/api/doc/create-qr-code/) and finally we have the [`QRicketProvider`](lib/Providers/Qr/QRicketProvider.php) which uses the [QRickit API](http://qrickit.com/qrickit_apps/qrickit_api.php). All three inherit from a common (abstract) baseclass named [`BaseHTTPQRCodeProvider`](lib/Providers/Qr/BaseHTTPQRCodeProvider.php) because all three share the same functionality: retrieve an image from a 3rd party over HTTP. All three classes have constructors that allow you to tweak some settings and most, if not all, arguments should speak for themselves. If you're not sure which values are supported, click the links in this paragraph for documentation on the API's that are utilized by these classes.
If you don't like any of the built-in classes because you don't want to rely on external resources for example or because you're paranoid about sending the TOTP secret to these 3rd parties (which is useless to them since they miss *at least one* other factor in the [MFA process](http://en.wikipedia.org/wiki/Multi-factor_authentication)), feel tree to implement your own. The `IQRCodeProvider` interface couldn't be any simpler. All you need to do is implement 2 methods:
````php
getMimeType();
getQRCodeImage($qrtext, $size);
````
The `getMimeType()` method should return the [MIME type](http://en.wikipedia.org/wiki/Internet_media_type) of the image that is returned by our implementation of `getQRCodeImage()`. In this example it's simply `image/png`. The `getQRCodeImage()` method is passed two arguments: `$qrtext` and `$size`. The latter, `$size`, is simply the width/height in pixels of the image desired by the caller. The first, `$qrtext` is the text that should be encoded in the QR-code. An example of such a text would be:
`otpauth://totp/LABEL:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=ISSUER`
All you need to do is return the QR-code as binary image data and you're done. All parts of the `$qrtext` have been escaped for you (but note: you *may* need to escape the entire `$qrtext` just once more when passing the data to another server as GET-parameter).
Let's see if we can use [PHP QR Code](http://phpqrcode.sourceforge.net/) to implement our own, custom, no-3rd-parties-allowed-here, provider. We start with downloading the [required (single) file](https://github.com/t0k4rt/phpqrcode/blob/master/phpqrcode.php) and putting it in the directory where `TwoFactorAuth.php` is located as well. Now let's implement the provider: create another file named `myprovider.php` in the `Providers\Qr` directory and paste in this content:
````php
<?php
require_once '../../phpqrcode.php'; // Yeah, we're gonna need that
namespace RobThree\Auth\Providers\Qr;
class MyProvider implements IQRCodeProvider {
public function getMimeType() {
return 'image/png'; // This provider only returns PNG's
}
public function getQRCodeImage($qrtext, $size) {
ob_start(); // 'Catch' QRCode's output
QRCode::png($qrtext, null, QR_ECLEVEL_L, 3, 4); // We ignore $size and set it to 3
// since phpqrcode doesn't support
// a size in pixels...
$result = ob_get_contents(); // 'Catch' QRCode's output
ob_end_clean(); // Cleanup
return $result; // Return image
}
}
````
That's it. We're done! We've implemented our own provider (with help of PHP QR Code). No more external dependencies, no more unnecessary latencies. Now let's *use* our provider:
````php
<?php
$mp = new RobThree\Auth\Providers\Qr\MyProvider();
$tfa = new RobThree\Auth\TwoFactorAuth('My Company', 6, 30, 'sha1', $mp);
$secret = $tfa->createSecret();
?>
<p><img src="<?php echo $tfa->getQRCodeImageAsDataUri('Bob Ross', $secret); ?>"></p>
````
Voilà. Couldn't make it any simpler.
### RNG providers
This library also comes with three 'built-in' RNG providers ([Random Number Generator](https://en.wikipedia.org/wiki/Random_number_generation)). The RNG provider generates a number of random bytes and returns these bytes as a string. These values are then used to create the secret. By default (no RNG provider specified) TwoFactorAuth will try to determine the best available RNG provider to use. It will, by default, try to use the [`CSRNGProvider`](lib/Providers/Rng/CSRNGProvider.php) for PHP7+ or the [`MCryptRNGProvider`](lib/Providers/Rng/MCryptRNGProvider.php); if this is not available/supported for any reason it will try to use the [`OpenSSLRNGProvider`](lib/Providers/Rng/OpenSSLRNGProvider.php) and if that is also not available/supported it will try to use the final RNG provider: [`HashRNGProvider`](lib/Providers/Rng/HashRNGProvider.php). Each of these providers use their own method of generating a random sequence of bytes. The first three (`CSRNGProvider`, `OpenSSLRNGProvider` and `MCryptRNGProvider`) return a [cryptographically secure](https://en.wikipedia.org/wiki/Cryptographically_secure_pseudorandom_number_generator) sequence of random bytes whereas the `HashRNGProvider` returns a **non-cryptographically secure** sequence.
You can easily implement your own `RNGProvider` by simply implementing the `IRNGProvider` interface. Each of the 'built-in' RNG providers have some constructor parameters that allow you to 'tweak' some of the settings to use when creating the random bytes such as which source to use (`MCryptRNGProvider`) or which hashing algorithm (`HashRNGProvider`). I encourage you to have a look at some of the ['built-in' RNG providers](lib/Providers/Rng) for details and the [`IRNGProvider` interface](lib/Providers/Rng/IRNGProvider.php).
### Time providers
Another set of providers in this library are the Time Providers; this library provides three 'built-in' ones. The default Time Provider used is the [`LocalMachineTimeProvider`](lib/Providers/Time/LocalMachineTimeProvider.php); this provider simply returns the output of `Time()` and is *highly recommended* as default provider. The [`HttpTimeProvider`](lib/Providers/Time/HttpTimeProvider.php) executes a `HEAD` request against a given webserver (default: google.com) and tries to extract the `Date:`-HTTP header and returns it's date. Other url's/domains can be used by specifying the url in the constructor. The final Time Provider is the [`ConvertUnixTimeDotComTimeProvider`](lib/Providers/Time/ConvertUnixTimeDotComTimeProvider.php) which does a HTTP request to `convert-unix-time.com/api` and decodes the `JSON` result to retrieve the time.
You can easily implement your own `TimeProvider` by simply implementing the `ITimeProvider` interface.
As to *why* these Time Providers are implemented: it allows the TwoFactorAuth library to ensure the hosts time is correct (or rather: within a margin). You can use the `ensureCorrectTime()` method to ensure the hosts time is correct. By default this method will compare the hosts time (returned by calling `time()` on the `LocalMachineTimeProvider`) to Google's and convert-unix-time.com's current time. You can pass an array of `ITimeProvider`s and specify the `leniency` (second argument) allowed (default: 5 seconds). The method will throw when the TwoFactorAuth's timeprovider (which can be any `ITimeProvider`, see constructor) differs more than the given amount of seconds from any of the given `ITimeProviders`. We advise to call this method sparingly when relying on 3rd parties (which both the `HttpTimeProvider` and `ConvertUnixTimeDotComTimeProvider` do) or, if you need to ensure time is correct on a (very) regular basis to implement an `ITimeProvider` that is more efficient than the 'built-in' ones (like use a GPS signal). The `ensureCorrectTime()` method is mostly to be used to make sure the server is configured correctly.
If you need more in-depth information about the configuration available then you can read through the rest of [documentation](https://robthree.github.io/TwoFactorAuth).
## Integrations
- [CakePHP 3](https://github.com/andrej-griniuk/cakephp-two-factor-auth)
- [CakePHP 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))
-69
View File
@@ -1,69 +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\GoogleQRCodeProvider.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\ConvertUnixTimeDotComTimeProvider.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\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" />
</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
+33 -4
View File
@@ -1,7 +1,6 @@
{
"name": "robthree/twofactorauth",
"description": "Two Factor Authentication",
"version": "1.6",
"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,10 +27,16 @@
"source": "https://github.com/RobThree/TwoFactorAuth"
},
"require": {
"php": ">=5.3.0"
"php": ">=8.2.0"
},
"require-dev": {
"phpunit/phpunit": "@stable"
"phpunit/phpunit": "^9",
"friendsofphp/php-cs-fixer": "^3.13",
"phpstan/phpstan": "^1.9"
},
"suggest": {
"bacon/bacon-qr-code": "Needed for BaconQrCodeProvider provider",
"endroid/qr-code": "Needed for EndroidQrCodeProvider"
},
"autoload": {
"psr-4": {
@@ -30,7 +45,21 @@
},
"autoload-dev": {
"psr-4": {
"RobThree\\Auth\\Test\\": "tests"
"Tests\\": "tests/"
}
},
"scripts": {
"phpstan": [
"phpstan analyze --xdebug lib tests testsDependency"
],
"lint": [
"php-cs-fixer fix -v"
],
"lint-ci": [
"PHP_CS_FIXER_IGNORE_ENV=1 php-cs-fixer fix -v --dry-run --stop-on-violation"
],
"test": [
"XDEBUG_MODE=coverage phpunit"
]
}
}
Generated
-980
View File
@@ -1,980 +0,0 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"content-hash": "9647de85f54ba6db237f5ff42ff85a1f",
"packages": [],
"packages-dev": [
{
"name": "doctrine/instantiator",
"version": "1.0.5",
"source": {
"type": "git",
"url": "https://github.com/doctrine/instantiator.git",
"reference": "8e884e78f9f0eb1329e445619e04456e64d8051d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d",
"reference": "8e884e78f9f0eb1329e445619e04456e64d8051d",
"shasum": ""
},
"require": {
"php": ">=5.3,<8.0-DEV"
},
"require-dev": {
"athletic/athletic": "~0.1.8",
"ext-pdo": "*",
"ext-phar": "*",
"phpunit/phpunit": "~4.0",
"squizlabs/php_codesniffer": "~2.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Marco Pivetta",
"email": "ocramius@gmail.com",
"homepage": "http://ocramius.github.com/"
}
],
"description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
"homepage": "https://github.com/doctrine/instantiator",
"keywords": [
"constructor",
"instantiate"
],
"time": "2015-06-14T21:17:01+00:00"
},
{
"name": "phpdocumentor/reflection-docblock",
"version": "2.0.4",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
"reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/d68dbdc53dc358a816f00b300704702b2eaff7b8",
"reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"require-dev": {
"phpunit/phpunit": "~4.0"
},
"suggest": {
"dflydev/markdown": "~1.0",
"erusev/parsedown": "~1.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
}
},
"autoload": {
"psr-0": {
"phpDocumentor": [
"src/"
]
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mike van Riel",
"email": "mike.vanriel@naenius.com"
}
],
"time": "2015-02-03T12:10:50+00:00"
},
{
"name": "phpspec/prophecy",
"version": "v1.6.2",
"source": {
"type": "git",
"url": "https://github.com/phpspec/prophecy.git",
"reference": "6c52c2722f8460122f96f86346600e1077ce22cb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpspec/prophecy/zipball/6c52c2722f8460122f96f86346600e1077ce22cb",
"reference": "6c52c2722f8460122f96f86346600e1077ce22cb",
"shasum": ""
},
"require": {
"doctrine/instantiator": "^1.0.2",
"php": "^5.3|^7.0",
"phpdocumentor/reflection-docblock": "^2.0|^3.0.2",
"sebastian/comparator": "^1.1",
"sebastian/recursion-context": "^1.0|^2.0"
},
"require-dev": {
"phpspec/phpspec": "^2.0",
"phpunit/phpunit": "^4.8 || ^5.6.5"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.6.x-dev"
}
},
"autoload": {
"psr-0": {
"Prophecy\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Konstantin Kudryashov",
"email": "ever.zet@gmail.com",
"homepage": "http://everzet.com"
},
{
"name": "Marcello Duarte",
"email": "marcello.duarte@gmail.com"
}
],
"description": "Highly opinionated mocking framework for PHP 5.3+",
"homepage": "https://github.com/phpspec/prophecy",
"keywords": [
"Double",
"Dummy",
"fake",
"mock",
"spy",
"stub"
],
"time": "2016-11-21T14:58:47+00:00"
},
{
"name": "phpunit/php-code-coverage",
"version": "2.2.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/eabf68b476ac7d0f73793aada060f1c1a9bf8979",
"reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979",
"shasum": ""
},
"require": {
"php": ">=5.3.3",
"phpunit/php-file-iterator": "~1.3",
"phpunit/php-text-template": "~1.2",
"phpunit/php-token-stream": "~1.3",
"sebastian/environment": "^1.3.2",
"sebastian/version": "~1.0"
},
"require-dev": {
"ext-xdebug": ">=2.1.4",
"phpunit/phpunit": "~4"
},
"suggest": {
"ext-dom": "*",
"ext-xdebug": ">=2.2.1",
"ext-xmlwriter": "*"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.2.x-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sb@sebastian-bergmann.de",
"role": "lead"
}
],
"description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
"homepage": "https://github.com/sebastianbergmann/php-code-coverage",
"keywords": [
"coverage",
"testing",
"xunit"
],
"time": "2015-10-06T15:47:00+00:00"
},
{
"name": "phpunit/php-file-iterator",
"version": "1.4.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-file-iterator.git",
"reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3cc8f69b3028d0f96a9078e6295d86e9bf019be5",
"reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.4.x-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sb@sebastian-bergmann.de",
"role": "lead"
}
],
"description": "FilterIterator implementation that filters files based on a list of suffixes.",
"homepage": "https://github.com/sebastianbergmann/php-file-iterator/",
"keywords": [
"filesystem",
"iterator"
],
"time": "2016-10-03T07:40:28+00:00"
},
{
"name": "phpunit/php-text-template",
"version": "1.2.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-text-template.git",
"reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
"reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"type": "library",
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de",
"role": "lead"
}
],
"description": "Simple template engine.",
"homepage": "https://github.com/sebastianbergmann/php-text-template/",
"keywords": [
"template"
],
"time": "2015-06-21T13:50:34+00:00"
},
{
"name": "phpunit/php-timer",
"version": "1.0.8",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-timer.git",
"reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/38e9124049cf1a164f1e4537caf19c99bf1eb260",
"reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"require-dev": {
"phpunit/phpunit": "~4|~5"
},
"type": "library",
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sb@sebastian-bergmann.de",
"role": "lead"
}
],
"description": "Utility class for timing",
"homepage": "https://github.com/sebastianbergmann/php-timer/",
"keywords": [
"timer"
],
"time": "2016-05-12T18:03:57+00:00"
},
{
"name": "phpunit/php-token-stream",
"version": "1.4.9",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-token-stream.git",
"reference": "3b402f65a4cc90abf6e1104e388b896ce209631b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/3b402f65a4cc90abf6e1104e388b896ce209631b",
"reference": "3b402f65a4cc90abf6e1104e388b896ce209631b",
"shasum": ""
},
"require": {
"ext-tokenizer": "*",
"php": ">=5.3.3"
},
"require-dev": {
"phpunit/phpunit": "~4.2"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.4-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de"
}
],
"description": "Wrapper around PHP's tokenizer extension.",
"homepage": "https://github.com/sebastianbergmann/php-token-stream/",
"keywords": [
"tokenizer"
],
"time": "2016-11-15T14:06:22+00:00"
},
{
"name": "phpunit/phpunit",
"version": "4.8.35",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "791b1a67c25af50e230f841ee7a9c6eba507dc87"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/791b1a67c25af50e230f841ee7a9c6eba507dc87",
"reference": "791b1a67c25af50e230f841ee7a9c6eba507dc87",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-json": "*",
"ext-pcre": "*",
"ext-reflection": "*",
"ext-spl": "*",
"php": ">=5.3.3",
"phpspec/prophecy": "^1.3.1",
"phpunit/php-code-coverage": "~2.1",
"phpunit/php-file-iterator": "~1.4",
"phpunit/php-text-template": "~1.2",
"phpunit/php-timer": "^1.0.6",
"phpunit/phpunit-mock-objects": "~2.3",
"sebastian/comparator": "~1.2.2",
"sebastian/diff": "~1.2",
"sebastian/environment": "~1.3",
"sebastian/exporter": "~1.2",
"sebastian/global-state": "~1.0",
"sebastian/version": "~1.0",
"symfony/yaml": "~2.1|~3.0"
},
"suggest": {
"phpunit/php-invoker": "~1.1"
},
"bin": [
"phpunit"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.8.x-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de",
"role": "lead"
}
],
"description": "The PHP Unit Testing framework.",
"homepage": "https://phpunit.de/",
"keywords": [
"phpunit",
"testing",
"xunit"
],
"time": "2017-02-06T05:18:07+00:00"
},
{
"name": "phpunit/phpunit-mock-objects",
"version": "2.3.8",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git",
"reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/ac8e7a3db35738d56ee9a76e78a4e03d97628983",
"reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983",
"shasum": ""
},
"require": {
"doctrine/instantiator": "^1.0.2",
"php": ">=5.3.3",
"phpunit/php-text-template": "~1.2",
"sebastian/exporter": "~1.2"
},
"require-dev": {
"phpunit/phpunit": "~4.4"
},
"suggest": {
"ext-soap": "*"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.3.x-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sb@sebastian-bergmann.de",
"role": "lead"
}
],
"description": "Mock Object library for PHPUnit",
"homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/",
"keywords": [
"mock",
"xunit"
],
"time": "2015-10-02T06:51:40+00:00"
},
{
"name": "sebastian/comparator",
"version": "1.2.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git",
"reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2b7424b55f5047b47ac6e5ccb20b2aea4011d9be",
"reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be",
"shasum": ""
},
"require": {
"php": ">=5.3.3",
"sebastian/diff": "~1.2",
"sebastian/exporter": "~1.2 || ~2.0"
},
"require-dev": {
"phpunit/phpunit": "~4.4"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.2.x-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Jeff Welch",
"email": "whatthejeff@gmail.com"
},
{
"name": "Volker Dusch",
"email": "github@wallbash.com"
},
{
"name": "Bernhard Schussek",
"email": "bschussek@2bepublished.at"
},
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de"
}
],
"description": "Provides the functionality to compare PHP values for equality",
"homepage": "http://www.github.com/sebastianbergmann/comparator",
"keywords": [
"comparator",
"compare",
"equality"
],
"time": "2017-01-29T09:50:25+00:00"
},
{
"name": "sebastian/diff",
"version": "1.4.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/diff.git",
"reference": "13edfd8706462032c2f52b4b862974dd46b71c9e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/13edfd8706462032c2f52b4b862974dd46b71c9e",
"reference": "13edfd8706462032c2f52b4b862974dd46b71c9e",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"require-dev": {
"phpunit/phpunit": "~4.8"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.4-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Kore Nordmann",
"email": "mail@kore-nordmann.de"
},
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de"
}
],
"description": "Diff implementation",
"homepage": "https://github.com/sebastianbergmann/diff",
"keywords": [
"diff"
],
"time": "2015-12-08T07:14:41+00:00"
},
{
"name": "sebastian/environment",
"version": "1.3.8",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/environment.git",
"reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/be2c607e43ce4c89ecd60e75c6a85c126e754aea",
"reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea",
"shasum": ""
},
"require": {
"php": "^5.3.3 || ^7.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8 || ^5.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.3.x-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de"
}
],
"description": "Provides functionality to handle HHVM/PHP environments",
"homepage": "http://www.github.com/sebastianbergmann/environment",
"keywords": [
"Xdebug",
"environment",
"hhvm"
],
"time": "2016-08-18T05:49:44+00:00"
},
{
"name": "sebastian/exporter",
"version": "1.2.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/exporter.git",
"reference": "42c4c2eec485ee3e159ec9884f95b431287edde4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/42c4c2eec485ee3e159ec9884f95b431287edde4",
"reference": "42c4c2eec485ee3e159ec9884f95b431287edde4",
"shasum": ""
},
"require": {
"php": ">=5.3.3",
"sebastian/recursion-context": "~1.0"
},
"require-dev": {
"ext-mbstring": "*",
"phpunit/phpunit": "~4.4"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.3.x-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Jeff Welch",
"email": "whatthejeff@gmail.com"
},
{
"name": "Volker Dusch",
"email": "github@wallbash.com"
},
{
"name": "Bernhard Schussek",
"email": "bschussek@2bepublished.at"
},
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de"
},
{
"name": "Adam Harvey",
"email": "aharvey@php.net"
}
],
"description": "Provides the functionality to export PHP variables for visualization",
"homepage": "http://www.github.com/sebastianbergmann/exporter",
"keywords": [
"export",
"exporter"
],
"time": "2016-06-17T09:04:28+00:00"
},
{
"name": "sebastian/global-state",
"version": "1.1.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/global-state.git",
"reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4",
"reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"require-dev": {
"phpunit/phpunit": "~4.2"
},
"suggest": {
"ext-uopz": "*"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de"
}
],
"description": "Snapshotting of global state",
"homepage": "http://www.github.com/sebastianbergmann/global-state",
"keywords": [
"global state"
],
"time": "2015-10-12T03:26:01+00:00"
},
{
"name": "sebastian/recursion-context",
"version": "1.0.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/recursion-context.git",
"reference": "913401df809e99e4f47b27cdd781f4a258d58791"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/913401df809e99e4f47b27cdd781f4a258d58791",
"reference": "913401df809e99e4f47b27cdd781f4a258d58791",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"require-dev": {
"phpunit/phpunit": "~4.4"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Jeff Welch",
"email": "whatthejeff@gmail.com"
},
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de"
},
{
"name": "Adam Harvey",
"email": "aharvey@php.net"
}
],
"description": "Provides functionality to recursively process PHP variables",
"homepage": "http://www.github.com/sebastianbergmann/recursion-context",
"time": "2015-11-11T19:50:13+00:00"
},
{
"name": "sebastian/version",
"version": "1.0.6",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/version.git",
"reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/version/zipball/58b3a85e7999757d6ad81c787a1fbf5ff6c628c6",
"reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6",
"shasum": ""
},
"type": "library",
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de",
"role": "lead"
}
],
"description": "Library that helps with managing the version number of Git-hosted PHP projects",
"homepage": "https://github.com/sebastianbergmann/version",
"time": "2015-06-21T13:59:46+00:00"
},
{
"name": "symfony/yaml",
"version": "v2.8.17",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "322a8c2dfbca15ad6b1b27e182899f98ec0e0153"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/322a8c2dfbca15ad6b1b27e182899f98ec0e0153",
"reference": "322a8c2dfbca15ad6b1b27e182899f98ec0e0153",
"shasum": ""
},
"require": {
"php": ">=5.3.9"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.8-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Component\\Yaml\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Yaml Component",
"homepage": "https://symfony.com",
"time": "2017-01-21T16:40:50+00:00"
}
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {
"phpunit/phpunit": 0
},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=5.3.0"
},
"platform-dev": []
}
+37 -19
View File
@@ -6,30 +6,48 @@
<body>
<ol>
<?php
require_once 'loader.php';
Loader::register('../lib','RobThree\\Auth');
// in practice you would require the composer loader if it was not already part of your framework or project
spl_autoload_register(function ($className) {
include_once str_replace(array('RobThree\\Auth', '\\'), array(__DIR__.'/../lib', '/'), $className) . '.php';
});
use \RobThree\Auth\TwoFactorAuth;
$tfa = new TwoFactorAuth('MyApp');
echo '<li>First create a secret and associate it with a user';
$secret = $tfa->createSecret(160); // Though the default is an 80 bits secret (for backwards compatibility reasons) we recommend creating 160+ bits secrets (see RFC 4226 - Algorithm Requirements)
echo '<li>Next create a QR code and let the user scan it:<br><img src="' . $tfa->getQRCodeImageAsDataUri('My label', $secret) . '"><br>...or display the secret to the user for manual entry: ' . chunk_split($secret, 4, ' ');
$code = $tfa->getCode($secret);
echo '<li>Next, have the user verify the code; at this time the code displayed by a 2FA-app would be: <span style="color:#00c">' . $code . '</span> (but that changes periodically)';
echo '<li>When the code checks out, 2FA can be / is enabled; store (encrypted?) secret with user and have the user verify a code each time a new session is started.';
echo '<li>When aforementioned code (' . $code . ') was entered, the result would be: ' . (($tfa->verifyCode($secret, $code) === true) ? '<span style="color:#0c0">OK</span>' : '<span style="color:#c00">FAIL</span>');
$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
$secret = $tfa->createSecret();
?>
<li>
Next create a QR code and let the user scan it:<br>
<img src="<?php echo $tfa->getQRCodeImageAsDataUri('Demo', $secret); ?>"><br>
...or display the secret to the user for manual entry:
<?php echo chunk_split($secret, 4, ' '); ?>
</li>
<?php
$code = $tfa->getCode($secret);
?>
<li>Next, have the user verify the code; at this time the code displayed by a 2FA-app would be: <span style="color:#00c"><?php echo $code; ?></span> (but that changes periodically)</li>
<li>When the code checks out, 2FA can be / is enabled; store (encrypted?) secret with user and have the user verify a code each time a new session is started.</li>
<li>
When aforementioned code (<?php echo $code; ?>) was entered, the result would be:
<?php if ($tfa->verifyCode($secret, $code) === true) { ?>
<span style="color:#0c0">OK</span>
<?php } else { ?>
<span style="color:#c00">FAIL</span>
<?php } ?>
</li>
</ol>
<p>Note: Make sure your server-time is <a href="http://en.wikipedia.org/wiki/Network_Time_Protocol">NTP-synced</a>! Depending on the $discrepancy allowed your time cannot drift too much from the users' time!</p>
<?php
try {
$tfa->ensureCorrectTime();
echo 'Your hosts time seems to be correct / within margin';
} catch (RobThree\Auth\TwoFactorAuthException $ex) {
echo '<b>Warning:</b> Your hosts time seems to be off: ' . $ex->getMessage();
}
try {
$tfa->ensureCorrectTime();
echo 'Your hosts time seems to be correct / within margin';
} catch (RobThree\Auth\TwoFactorAuthException $ex) {
echo '<b>Warning:</b> Your hosts time seems to be off: ' . $ex->getMessage();
}
?>
</body>
</html>
-50
View File
@@ -1,50 +0,0 @@
<?php
//http://www.leaseweblabs.com/2014/04/psr-0-psr-4-autoloading-classes-php/
class Loader
{
protected static $parentPath = null;
protected static $paths = null;
protected static $files = null;
protected static $nsChar = '\\';
protected static $initialized = false;
protected static function initialize()
{
if (static::$initialized) return;
static::$initialized = true;
static::$parentPath = __FILE__;
for ($i=substr_count(get_class(), static::$nsChar);$i>=0;$i--) {
static::$parentPath = dirname(static::$parentPath);
}
static::$paths = array();
static::$files = array(__FILE__);
}
public static function register($path,$namespace) {
if (!static::$initialized) static::initialize();
static::$paths[$namespace] = trim($path,DIRECTORY_SEPARATOR);
}
public static function load($class) {
if (class_exists($class,false)) return;
if (!static::$initialized) static::initialize();
foreach (static::$paths as $namespace => $path) {
if (!$namespace || $namespace.static::$nsChar === substr($class, 0, strlen($namespace.static::$nsChar))) {
$fileName = substr($class,strlen($namespace.static::$nsChar)-1);
$fileName = str_replace(static::$nsChar, DIRECTORY_SEPARATOR, ltrim($fileName,static::$nsChar));
$fileName = static::$parentPath.DIRECTORY_SEPARATOR.$path.DIRECTORY_SEPARATOR.$fileName.'.php';
if (file_exists($fileName)) {
include $fileName;
return true;
}
}
}
return false;
}
}
spl_autoload_register(array('Loader', 'load'));
+3
View File
@@ -0,0 +1,3 @@
theme: jekyll-theme-minimal
logo: https://raw.githubusercontent.com/RobThree/TwoFactorAuth/master/multifactorauthforeveryone.png
+9
View File
@@ -0,0 +1,9 @@
---
layout: default
---
<a href="{{ site.baseurl }}">← contents</a>
<h1>{{ page.title }}</h1>
{{ content }}
+54
View File
@@ -0,0 +1,54 @@
---
---
@import "{{ site.theme }}";
// undo some of the theme to allow code samples to be wider
header {
padding-right: 0;
}
@media print, screen and (min-width: 961px) {
header {
border: 1px solid #e5e5e5;
border-radius: 5px;
margin-bottom: 30px;
margin-right: 30px;
padding-top: 20px;
position: static;
text-align: center;
}
section {
float: none;
width: auto;
}
footer {
float: none;
position: static;
}
}
// ensure code samples can be really wide
.language-php.highlighter-rouge {
clear: both;
}
// add missing consistency
header img {
margin-bottom: 20px;
}
// quick navigation hack needs some spacing
section > a:first-child {
display: block;
margin-bottom:45px;
}
// 100% width is treated like clear which makes it look bad
table {
width: auto;
}
// reset document block whatever so the bullets aren't disturbed by the float
ul {
overflow: hidden;
}
+66
View File
@@ -0,0 +1,66 @@
---
layout: post
title: Getting Started
---
## 1. Installation
The best way of making use of this project is by installing it with [composer](https://getcomposer.org/doc/01-basic-usage.md).
```
composer require robthree/twofactorauth
```
## 2. Create an instance
`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
// 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");
```
## 3. Shared secrets
When your user is setting up two-factor, or multi-factor, authentication in your project, you can create a secret from the instance.
```php
$secret = $tfa->createSecret();
```
Once you have a secret, it can be communicated to the user however you wish.
```php
<p>Please enter the following code in your app: '<?php echo $secret; ?>'</p>
```
**Note:** until you have verified the user is able to use the secret properly, you should store the secret as part of the current session and not save the secret against your user record.
## 4. Verifying
Having provided the user with the secret, the best practice is to verify their authenticator app can create the appropriate code.
```php
$result = $tfa->verifyCode($secret, $_POST['verification']);
```
If `$result` is `true` then your user has been able to successfully record the `$secret` in their authenticator app and it has generated an appropriate code.
You can now save the `$secret` to your user record and use the same `verifyCode` method each time they log in.
+32
View File
@@ -0,0 +1,32 @@
---
layout: post
title: Improved Code Verification
---
When verifying codes that a user has entered, there are other optional arguments which can improve verification of the code.
```php
$result = $tfa->verifyCode($secret, $_POST['verification'], $discrepancy, $time, &$timeslice);
```
## Discrepancy (default 1)
As the codes that are generated and accepted are consistent within a certain time window (i.e. a timeslice, 30 seconds long by default), it is very important that the server (and the users authenticator app) have the correct time (and date).
The value of `$discrepancy` is the number of timeslices checked in **both** directions of the current one. So when the current time is `14:34:21`, the 'current timeslice' is `14:34:00` to `14:34:30`. If the default is left unchanged, we also verify the code against the timeslice of `14:33:30` to `14:34:00` and for `14:34:30` to `14:35:00`.
This should be sufficient for most cases however you can increase it if you wish. It would be unwise for this to be too high as it could allow a code to be valid for long enough that it could be used fraudulently.
## Time (default null)
The second, `$time`, allows you to check a code for a specific point in time. This argument has no real practical use but can be handy for unit testing. The default value, `null`, means: use the current time.
## Timeslice
`$timeslice` returns a value by reference. The value returned is the timeslice that matched the code (if any) or `0`.
You can store a timeslice alongside the secret and verify that any new timeslice is greater than the existing one.
i.e. if `verifyCode` returns true _and_ the returned timeslice is greater than the last used timeslice for this user/secret then this is the first time the code has been used and you should now store the higher timeslice to verify that the user.
This is an effective defense against a [replay attack](https://en.wikipedia.org/wiki/Replay_attack).
+18
View File
@@ -0,0 +1,18 @@
---
title: Contents
---
## [The Basics - Getting Started](getting-started.html)
## Advanced Usage
[QR Codes](qr-codes.html)
- [QRServerProvider](qr-codes/qr-server.html)
- [ImageChartsQRCodeProvider](qr-codes/image-charts.html)
- [QRicketProvider](qr-codes/qrickit.html)
- [EndroidQrCodeProvider](qr-codes/endroid.html) (and EndroidQrCodeWithLogoProvider)
- [BaconQRCodeProvider](qr-codes/bacon.html)
[Improved Code Verification](improved-code-verification.html)
[Other Optional Configuration](optional-configuration.html)
+47
View File
@@ -0,0 +1,47 @@
---
layout: post
title: Optional Configuration
---
## Instance Configuration
The instance (`new TwoFactorAuth()`) can only be configured by the constructor with the following optional arguments
Argument | Default value | Use
------------------|-------------------|-----
`$issuer` | `null` | Will be displayed in the users app as the default issuer name when using QR code to import the secret
`$digits` | `6` | The number of digits the resulting codes will be
`$period` | `30` | The number of seconds a code will be valid
`$algorithm` | `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
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
These allow the TwoFactorAuth library to ensure the servers time is correct (or at least within a margin).
You can use the `ensureCorrectTime()` method to ensure the hosts time is correct. By default this method will compare the hosts time (returned by calling `time()` on the `LocalMachineTimeProvider`) to the default `NTPTimeProvider` and `HttpTimeProvider`.
**Note:** the `NTPTimeProvider` requires your PHP to have the ability to create sockets. If you do not have that ability and wish to use this function, you should pass an array with only an instance of `HttpTimeProvider`.
Alternatively, you can pass an array of classes that implement the [`ITimeProvider` interface](https://github.com/RobThree/TwoFactorAuth/blob/master/lib/Providers/Time/ITimeProvider.php) to change this and specify the second argument, leniency in seconds (default: 5). An exception will be thrown if the time difference is greater than the leniency.
Ordinarily, you should not need to monitor that the time on the server is correct in this way however if you choose to, we advise to call this method sparingly when relying on 3rd parties (which both the `HttpTimeProvider` and `NTPTimeProvider` do) or, if you need to ensure time is correct on a (very) regular basis to implement an `ITimeProvider` that is more efficient than the built-in ones (making use of a GPS signal for example).
## Secret Configuration
Secrets can be optionally configured with the following optional arguments
Argument | Default value | Use
-----------------------|---------------|-----
`$bits` | `80` | The number of bits (related to the length of the secret)
`$requirecryptosecure` | `true` | Whether you want to require a cryptographically secure source of random numbers
**Note:** as above, these values provide the widest variety of support amongst common authenticator apps however you may choose to increase the value of `$bits` (160 or higher is recommended, see [RFC 4226 - Algorithm Requirements](https://tools.ietf.org/html/rfc4226#section-4)) as long as it is set to a multiple of 8.
+58
View File
@@ -0,0 +1,58 @@
---
layout: post
title: QR Codes
---
An alternative way of communicating the secret to the user is through the use of [QR Codes](http://en.wikipedia.org/wiki/QR_code) which most if not all authenticator mobile apps can scan.
This can avoid accidental typing errors and also pre-set some text values within the 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
````php
<p>Scan the following image with your app:</p>
<img src="<?php echo $tfa->getQRCodeImageAsDataUri('Bob Ross', $secret); ?>">
````
You can also specify a size as a third argument which is 200 by default.
## Offline Providers
[EndroidQrCodeProvider](qr-codes/endroid.md) and EndroidQrCodeWithLogoProvider
[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](../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.
Example:
```php
use RobThree\Auth\TwoFactorAuth;
// using a custom object implementing IQRCodeProvider
$tfa = new TwoFactorAuth(new MyQrCodeProvider());
// using named argument and a variable
$tfa = new TwoFactorAuth(qrcodeprovider: $qrGenerator);
```
## Online Providers
**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());
```
+23
View File
@@ -0,0 +1,23 @@
---
layout: post
title: bacon/bacon-qr-code
---
## Installation
In order to use this provider, you will need to install the library at version 2 (or later) and its dependencies
```
composer require bacon/bacon-qr-code ^2.0
```
You will also need the PHP imagick extension **if** you aren't using the SVG format.
## Optional Configuration
Argument | Default value
--------------------|---------------
`$borderWidth` | `4`
`$backgroundColour` | `'#ffffff'`
`$foregroundColour` | `'#000000'`
`$format` | `'png'`
+37
View File
@@ -0,0 +1,37 @@
---
layout: post
title: endroid/qr-code
---
## Installation
In order to use this provider, you will need to install the library at version 3 and its dependencies
```
composer require endroid/qr-code ^3.0
```
You will also need the PHP gd extension installing.
## Optional Configuration
Argument | Default value
------------------------|---------------
`$bgcolor` | `'ffffff'`
`$color` | `'000000'`
`$margin` | `0`
`$errorcorrectionlevel` | `'H'`
## Logo
If you make use of `EndroidQrCodeWithLogoProvider` then you have access to the `setLogo` function on the provider so you may add a logo to the centre of your QR code.
```php
use RobThree\Auth\Providers\Qr\EndroidQrCodeWithLogoProvider;
$qrCodeProvider = new EndroidQrCodeWithLogoProvider();
$qrCodeProvider->setLogo('/path/to/your/image');
```
You can see how to also set the size of the logo in the [source code](https://github.com/RobThree/TwoFactorAuth/blob/master/lib/Providers/Qr/EndroidQrCodeWithLogoProvider.php).
+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'`
+16
View File
@@ -0,0 +1,16 @@
---
layout: post
title: Image-Charts
---
## Optional Configuration
Argument | Default value
------------------------|---------------
`$verifyssl` | `true`
`$errorcorrectionlevel` | `'L'`
`$margin` | `4`
`$verifyssl` is used internally to help guarantee the security of the connection. It is possible that where you are running the code from will have problems verifying an SSL connection so if you know this is not the case, you can supply `true`.
The other parameters are passed to [Image-Charts](https://documentation.image-charts.com/qr-codes/) so you can refer to them for more detail on how the values are used.
+20
View File
@@ -0,0 +1,20 @@
---
layout: post
title: QR Server
---
## Optional Configuration
Argument | Default value
------------------------|---------------
`$verifyssl` | `true`
`$errorcorrectionlevel` | `'L'`
`$margin` | `4`
`$qzone` | `1`
`$bgcolor` | `'ffffff'`
`$color` | `'000000'`
`$format` | `'png'`
`$verifyssl` is used internally to help guarantee the security of the connection. It is possible that where you are running the code from will have problems verifying an SSL connection so if you know this is not the case, you can supply `true`.
The other parameters are passed to [goqr.me](http://goqr.me/api/doc/create-qr-code/) so you can refer to them for more detail on how the values are used.
+15
View File
@@ -0,0 +1,15 @@
---
layout: post
title: QRickit
---
## Optional Configuration
Argument | Default value
------------------------|---------------
`$errorcorrectionlevel` | `'L'`
`$bgcolor` | `'ffffff'`
`$color` | `'000000'`
`$format` | `'png'`
The parameters are passed to [QRickit](http://qrickit.com/qrickit_apps/qrickit_api.php) so you can refer to them for more detail on how the values are used.
+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';
}
+141
View File
@@ -0,0 +1,141 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth\Providers\Qr;
use BaconQrCode\Renderer\Color\Rgb;
use BaconQrCode\Renderer\Image\EpsImageBackEnd;
use BaconQrCode\Renderer\Image\ImageBackEndInterface;
use BaconQrCode\Renderer\Image\ImagickImageBackEnd;
use BaconQrCode\Renderer\Image\SvgImageBackEnd;
use BaconQrCode\Renderer\ImageRenderer;
use BaconQrCode\Renderer\RendererStyle\EyeFill;
use BaconQrCode\Renderer\RendererStyle\Fill;
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
use BaconQrCode\Writer;
use RuntimeException;
class BaconQrCodeProvider implements IQRCodeProvider
{
/**
* Ensure we using the latest Bacon QR Code and specify default options
*/
public function __construct(
private 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);
}
public function getMimeType(): string
{
switch ($this->format) {
case 'png':
return 'image/png';
case 'gif':
return 'image/gif';
case 'jpg':
case 'jpeg':
return 'image/jpeg';
case 'svg':
return 'image/svg+xml';
case 'eps':
return 'application/postscript';
}
throw new RuntimeException(sprintf('Unknown MIME-type: %s', $this->format));
}
public function getQRCodeImage(string $qrText, int $size): string
{
$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') {
$svg = explode("\n", $output);
return $svg[1];
}
return $output;
}
/**
* Abstract QR code generation function
* providing colour changing support
*/
private function getQRCodeByBackend($qrText, $size, ImageBackEndInterface $backend)
{
$rendererStyleArgs = array($size, $this->borderWidth);
if (is_array($this->foregroundColour) && is_array($this->backgroundColour)) {
$rendererStyleArgs = array(...$rendererStyleArgs, ...array(
null,
null,
Fill::withForegroundColor(
new Rgb(...$this->backgroundColour),
new Rgb(...$this->foregroundColour),
new EyeFill(null, null),
new EyeFill(null, null),
new EyeFill(null, null)
),
));
}
$writer = new Writer(new ImageRenderer(
new RendererStyle(...$rendererStyleArgs),
$backend
));
return $writer->writeString($qrText);
}
/**
* Ensure colour is an array of three values but also
* accept a string and assume its a 3 or 6 character hex
*/
private function handleColour(array|string $colour): array|string
{
if (is_string($colour) && $colour[0] == '#') {
$hexToRGB = 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 #');
}
// split the array into three chunks
$split = str_split($input, strlen($input) / 3);
// cope with three character hex reference
if (strlen($input) == 3) {
array_walk($split, static function (&$character) {
$character = str_repeat($character, 2);
});
}
// convert hex to rgb
return array_map('hexdec', $split);
};
return $hexToRGB($colour);
}
if (is_array($colour) && count($colour) == 3) {
return $colour;
}
throw new RuntimeException('Invalid colour value');
}
}
+11 -7
View File
@@ -1,15 +1,17 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth\Providers\Qr;
abstract class BaseHTTPQRCodeProvider implements IQRCodeProvider
{
protected $verifyssl;
protected bool $verifyssl = true;
protected function getContent($url)
protected function getContent(string $url): string
{
$curlhandle = curl_init();
curl_setopt_array($curlhandle, array(
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
@@ -17,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);
curl_close($curlhandle);
if ($data === false) {
throw new QRException(curl_error($curlhandle));
}
return $data;
}
}
}
+123
View File
@@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth\Providers\Qr;
use Endroid\QrCode\Color\Color;
use Endroid\QrCode\ErrorCorrectionLevel;
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelHigh;
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelInterface;
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelLow;
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelMedium;
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelQuartile;
use Endroid\QrCode\QrCode;
use Endroid\QrCode\Writer\PngWriter;
class EndroidQrCodeProvider implements IQRCodeProvider
{
public $bgcolor;
public $color;
public $margin;
public $errorcorrectionlevel;
protected $endroid4 = false;
protected $endroid5 = false;
protected $endroid6 = false;
public function __construct($bgcolor = 'ffffff', $color = '000000', $margin = 0, $errorcorrectionlevel = 'H')
{
$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);
$this->margin = $margin;
$this->errorcorrectionlevel = $this->handleErrorCorrectionLevel($errorcorrectionlevel);
}
public function getMimeType(): string
{
return 'image/png';
}
public function getQRCodeImage(string $qrText, int $size): string
{
if (!$this->endroid4) {
return $this->qrCodeInstance($qrText, $size)->writeString();
}
$writer = new PngWriter();
return $writer->write($this->qrCodeInstance($qrText, $size))->getString();
}
protected function qrCodeInstance(string $qrText, int $size): QrCode
{
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(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) : array('r' => $r, 'g' => $g, 'b' => $b, 'a' => 0);
}
private function handleErrorCorrectionLevel(string $level): ErrorCorrectionLevelInterface|ErrorCorrectionLevel
{
// 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(),
};
}
}
+65
View File
@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth\Providers\Qr;
use Endroid\QrCode\Logo\Logo;
use Endroid\QrCode\QrCode;
use Endroid\QrCode\Writer\PngWriter;
class EndroidQrCodeWithLogoProvider extends EndroidQrCodeProvider
{
protected $logoPath;
protected $logoSize;
/**
* Adds an image to the middle of the QR Code.
* @param string $path Path to an image file
* @param array|int $size Just the width, or [width, height]
*/
public function setLogo($path, $size = null)
{
$this->logoPath = $path;
$this->logoSize = (array)$size;
}
public function getQRCodeImage(string $qrText, int $size): string
{
if (!$this->endroid4) {
return $this->qrCodeInstance($qrText, $size)->writeString();
}
$logo = null;
if ($this->logoPath) {
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();
}
protected function qrCodeInstance(string $qrText, int $size): QrCode
{
$qrCode = parent::qrCodeInstance($qrText, $size);
if (!$this->endroid4 && $this->logoPath) {
$qrCode->setLogoPath($this->logoPath);
if ($this->logoSize) {
$qrCode->setLogoSize($this->logoSize[0], $this->logoSize[1] ?? null);
}
}
return $qrCode;
}
}
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth\Providers\Qr;
// https://developers.google.com/chart/infographics/docs/qr_codes
class GoogleChartsQrCodeProvider extends BaseHTTPQRCodeProvider
{
public function __construct(protected bool $verifyssl = true, public string $errorcorrectionlevel = 'L', public int $margin = 4, public string $encoding = 'UTF-8')
{
}
public function getMimeType(): string
{
return 'image/png';
}
public function getQRCodeImage(string $qrText, int $size): string
{
return $this->getContent($this->getUrl($qrText, $size));
}
public function getUrl(string $qrText, int $size): string
{
$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);
}
}
-39
View File
@@ -1,39 +0,0 @@
<?php
namespace RobThree\Auth\Providers\Qr;
// https://developers.google.com/chart/infographics/docs/qr_codes
class GoogleQRCodeProvider extends BaseHTTPQRCodeProvider
{
public $errorcorrectionlevel;
public $margin;
function __construct($verifyssl = false, $errorcorrectionlevel = 'L', $margin = 1)
{
if (!is_bool($verifyssl))
throw new \QRException('VerifySSL must be bool');
$this->verifyssl = $verifyssl;
$this->errorcorrectionlevel = $errorcorrectionlevel;
$this->margin = $margin;
}
public function getMimeType()
{
return 'image/png';
}
public function getQRCodeImage($qrtext, $size)
{
return $this->getContent($this->getUrl($qrtext, $size));
}
public function getUrl($qrtext, $size)
{
return 'https://chart.googleapis.com/chart?cht=qr'
. '&chs=' . $size . 'x' . $size
. '&chld=' . $this->errorcorrectionlevel . '|' . $this->margin
. '&chl=' . rawurlencode($qrtext);
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth\Providers\Qr;
use function base64_decode;
use function preg_match;
trait HandlesDataUri
{
/**
* @return array<string, string>|null
*/
private function DecodeDataUri(string $datauri): ?array
{
if (preg_match('/data:(?P<mimetype>[\w\.\-\+\/]+);(?P<encoding>\w+),(?P<data>.*)/', $datauri, $m) === 1) {
return array(
'mimetype' => $m['mimetype'],
'encoding' => $m['encoding'],
'data' => base64_decode($m['data'], true),
);
}
return null;
}
}
+18 -3
View File
@@ -1,9 +1,24 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth\Providers\Qr;
interface IQRCodeProvider
{
public function getQRCodeImage($qrtext, $size);
public function getMimeType();
}
/**
* Generate and return the QR code to embed in a web page
*
* @param string $qrText the value to encode in the QR code
* @param int $size the desired size of the QR code
*
* @return string file contents of the QR code
*/
public function getQRCodeImage(string $qrText, int $size): string;
/**
* Returns the appropriate mime type for the QR code
* that will be generated
*/
public function getMimeType(): string;
}
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth\Providers\Qr;
/**
* Use https://image-charts.com to provide a QR code
*/
class ImageChartsQRCodeProvider extends BaseHTTPQRCodeProvider
{
public function __construct(protected bool $verifyssl = true, public string $errorcorrectionlevel = 'L', public int $margin = 1)
{
}
public function getMimeType(): string
{
return 'image/png';
}
public function getQRCodeImage(string $qrText, int $size): string
{
return $this->getContent($this->getUrl($qrText, $size));
}
public function getUrl(string $qrText, int $size): string
{
$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);
}
}
+7 -1
View File
@@ -1,5 +1,11 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth\Providers\Qr;
use RobThree\Auth\TwoFactorAuthException;
class QRException extends TwoFactorAuthException {}
class QRException extends TwoFactorAuthException
{
}
+38 -50
View File
@@ -1,71 +1,59 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth\Providers\Qr;
// http://goqr.me/api/doc/create-qr-code/
class QRServerProvider extends BaseHTTPQRCodeProvider
/**
* Use https://goqr.me/api/doc/create-qr-code/ to get QR code
*/
class QRServerProvider extends BaseHTTPQRCodeProvider
{
public $errorcorrectionlevel;
public $margin;
public $qzone;
public $bgcolor;
public $color;
public $format;
function __construct($verifyssl = false, $errorcorrectionlevel = 'L', $margin = 4, $qzone = 1, $bgcolor = 'ffffff', $color = '000000', $format = 'png')
public function __construct(protected bool $verifyssl = 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;
}
public function getMimeType()
public function getMimeType(): string
{
switch (strtolower($this->format))
{
case 'png':
switch (strtolower($this->format)) {
case 'png':
return 'image/png';
case 'gif':
case 'gif':
return 'image/gif';
case 'jpg':
case 'jpeg':
case 'jpg':
case 'jpeg':
return 'image/jpeg';
case 'svg':
case 'svg':
return 'image/svg+xml';
case 'eps':
case 'eps':
return 'application/postscript';
}
throw new \QRException(sprintf('Unknown MIME-type: %s', $this->format));
throw new QRException(sprintf('Unknown MIME-type: %s', $this->format));
}
public function getQRCodeImage($qrtext, $size)
public function getQRCodeImage(string $qrText, int $size): string
{
return $this->getContent($this->getUrl($qrtext, $size));
return $this->getContent($this->getUrl($qrText, $size));
}
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);
}
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'));
}
}
}
+30 -37
View File
@@ -1,54 +1,47 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth\Providers\Qr;
// http://qrickit.com/qrickit_apps/qrickit_api.php
class QRicketProvider extends BaseHTTPQRCodeProvider
/**
* Use http://qrickit.com/qrickit_apps/qrickit_api.php to provide a QR code
*/
class QRicketProvider extends BaseHTTPQRCodeProvider
{
public $errorcorrectionlevel;
public $margin;
public $qzone;
public $bgcolor;
public $color;
public $format;
function __construct($errorcorrectionlevel = 'L', $bgcolor = 'ffffff', $color = '000000', $format = 'p')
public function __construct(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;
}
public function getMimeType()
public function getMimeType(): string
{
switch (strtolower($this->format))
{
case 'p':
switch (strtolower($this->format)) {
case 'p':
return 'image/png';
case 'g':
case 'g':
return 'image/gif';
case 'j':
case 'j':
return 'image/jpeg';
}
throw new \QRException(sprintf('Unknown MIME-type: %s', $this->format));
throw new QRException(sprintf('Unknown MIME-type: %s', $this->format));
}
public function getQRCodeImage($qrtext, $size)
public function getQRCodeImage(string $qrText, int $size): string
{
return $this->getContent($this->getUrl($qrtext, $size));
return $this->getContent($this->getUrl($qrText, $size));
}
public function getUrl($qrtext, $size)
public function getUrl(string $qrText, int $size): string
{
return 'http://qrickit.com/api/qr'
. '?qrsize=' . $size
. '&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);
}
}
}
+8 -6
View File
@@ -1,14 +1,16 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth\Providers\Rng;
class CSRNGProvider implements IRNGProvider
{
public function getRandomBytes($bytecount) {
/**
* {@inheritdoc}
*/
public function getRandomBytes(int $bytecount): string
{
return random_bytes($bytecount); // PHP7+
}
public function isCryptographicallySecure() {
return true;
}
}
}
-28
View File
@@ -1,28 +0,0 @@
<?php
namespace RobThree\Auth\Providers\Rng;
class HashRNGProvider implements IRNGProvider
{
private $algorithm;
function __construct($algorithm = 'sha256' ) {
$algos = array_values(hash_algos());
if (!in_array($algorithm, $algos, true))
throw new \RNGException('Unsupported algorithm specified');
$this->algorithm = $algorithm;
}
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, sizeof($hash))];
}
return $result;
}
public function isCryptographicallySecure() {
return false;
}
}
+4 -3
View File
@@ -1,9 +1,10 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth\Providers\Rng;
interface IRNGProvider
{
public function getRandomBytes($bytecount);
public function isCryptographicallySecure();
}
public function getRandomBytes(int $bytecount): string;
}
-23
View File
@@ -1,23 +0,0 @@
<?php
namespace RobThree\Auth\Providers\Rng;
class MCryptRNGProvider implements IRNGProvider
{
private $source;
function __construct($source = MCRYPT_DEV_URANDOM) {
$this->source = $source;
}
public function getRandomBytes($bytecount) {
$result = mcrypt_create_iv($bytecount, $this->source);
if ($result === false)
throw new \RNGException('mcrypt_create_iv returned an invalid value');
return $result;
}
public function isCryptographicallySecure() {
return true;
}
}
-25
View File
@@ -1,25 +0,0 @@
<?php
namespace RobThree\Auth\Providers\Rng;
class OpenSSLRNGProvider implements IRNGProvider
{
private $requirestrong;
function __construct($requirestrong = true) {
$this->requirestrong = $requirestrong;
}
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;
}
public function isCryptographicallySecure() {
return $this->requirestrong;
}
}
+7 -1
View File
@@ -1,5 +1,11 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth\Providers\Rng;
use RobThree\Auth\TwoFactorAuthException;
class RNGException extends TwoFactorAuthException {}
class RNGException extends TwoFactorAuthException
{
}
@@ -1,15 +0,0 @@
<?php
namespace RobThree\Auth\Providers\Time;
class ConvertUnixTimeDotComTimeProvider implements ITimeProvider
{
public function getTime() {
$json = @json_decode(
@file_get_contents('http://www.convert-unix-time.com/api?timestamp=now')
);
if ($json === null || !is_int($json->timestamp))
throw new \TimeException('Unable to retrieve time from convert-unix-time.com');
return $json->timestamp;
}
}
+30 -21
View File
@@ -1,21 +1,25 @@
<?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
{
public $url;
public $options;
public $expectedtimeformat;
function __construct($url = 'https://google.com', $expectedtimeformat = 'D, d M Y H:i:s O+', array $options = null)
{
$this->url = $url;
$this->expectedtimeformat = $expectedtimeformat;
$this->options = $options;
/**
* @param array<string, mixed> $options
*/
public function __construct(
public string $url = 'https://google.com',
public string $expectedtimeformat = 'D, d M Y H:i:s O+',
public ?array $options = null,
) {
if ($this->options === null) {
$this->options = array(
'http' => array(
@@ -26,28 +30,33 @@ class HttpTimeProvider implements ITimeProvider
'request_fulluri' => true,
'header' => array(
'Connection: close',
'User-agent: TwoFactorAuth HttpTimeProvider (https://github.com/RobThree/TwoFactorAuth)'
)
)
'User-agent: TwoFactorAuth HttpTimeProvider (https://github.com/RobThree/TwoFactorAuth)',
'Cache-Control: no-cache',
),
),
);
}
}
public function getTime() {
/**
* {@inheritdoc}
*/
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);
foreach ($headers['wrapper_data'] as $h) {
if (strcasecmp(substr($h, 0, 5), 'Date:') === 0)
return \DateTime::createFromFormat($this->expectedtimeformat, trim(substr($h,5)))->getTimestamp();
if (strcasecmp(substr($h, 0, 5), 'Date:') === 0) {
return DateTime::createFromFormat($this->expectedtimeformat, trim(substr($h, 5)))->getTimestamp();
}
}
throw new \TimeException(sprintf('Unable to retrieve time from %s (Invalid or no "Date:" header found)', $this->url));
}
catch (Exception $ex) {
throw new \TimeException(sprintf('Unable to retrieve time from %s (%s)', $this->url, $ex->getMessage()));
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()));
}
}
}
}
+6 -1
View File
@@ -1,8 +1,13 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth\Providers\Time;
interface ITimeProvider
{
/**
* @return int the current timestamp according to this provider
*/
public function getTime();
}
}
@@ -1,9 +1,13 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth\Providers\Time;
class LocalMachineTimeProvider implements ITimeProvider {
public function getTime() {
class LocalMachineTimeProvider implements ITimeProvider
{
public function getTime()
{
return time();
}
}
}
+58
View File
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth\Providers\Time;
use Exception;
use function socket_create;
/**
* Takes the time from any NTP server
*/
class NTPTimeProvider implements ITimeProvider
{
public function __construct(public string $host = 'time.google.com', public int $port = 123, public int $timeout = 1)
{
if ($this->port <= 0 || $this->port > 65535) {
throw new TimeException('Port must be 0 < port < 65535');
}
if ($this->timeout < 0) {
throw new TimeException('Timeout must be >= 0');
}
}
/**
* {@inheritdoc}
*/
public function getTime()
{
try {
// Create a socket and connect to NTP server
$sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
socket_set_option($sock, SOL_SOCKET, SO_RCVTIMEO, array('sec' => $this->timeout, 'usec' => 0));
socket_connect($sock, $this->host, $this->port);
// Send request
$msg = "\010" . str_repeat("\0", 47);
socket_send($sock, $msg, strlen($msg), 0);
// Receive response and close socket
if (socket_recv($sock, $recv, 48, MSG_WAITALL) === false) {
throw new Exception(socket_strerror(socket_last_error($sock)));
}
socket_close($sock);
// Interpret response
$data = unpack('N12', $recv);
$timestamp = (int)sprintf('%u', $data[9]);
// NTP is number of seconds since 0000 UT on 1 January 1900 Unix time is seconds since 0000 UT on 1 January 1970
return $timestamp - 2208988800;
} catch (Exception $ex) {
throw new TimeException(sprintf('Unable to retrieve time from %s (%s)', $this->host, $ex->getMessage()));
}
}
}
+7 -1
View File
@@ -1,5 +1,11 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth\Providers\Time;
use RobThree\Auth\TwoFactorAuthException;
class TimeException extends TwoFactorAuthException {}
class TimeException extends TwoFactorAuthException
{
}
+112 -150
View File
@@ -1,44 +1,48 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth;
use function hash_equals;
use RobThree\Auth\Providers\Qr\IQRCodeProvider;
use RobThree\Auth\Providers\Rng\CSRNGProvider;
use RobThree\Auth\Providers\Rng\IRNGProvider;
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
{
private $algorithm;
private $period;
private $digits;
private $issuer;
private $qrcodeprovider = null;
private $rngprovider = null;
private $timeprovider = null;
private static $_base32dict = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567=';
private static $_base32;
private static $_base32lookup = array();
private static $_supportedalgos = array('sha1', 'sha256', 'sha512', 'md5');
private static string $_base32dict = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567=';
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');
$this->digits = $digits;
/** @var array<string> */
private static array $_base32;
if (!is_int($period) || $period <= 0)
/** @var array<string, int> */
private static array $_base32lookup = array();
public function __construct(
private IQRCodeProvider $qrcodeprovider,
private readonly ?string $issuer = null,
private readonly int $digits = 6,
private readonly int $period = 30,
private readonly Algorithm $algorithm = Algorithm::Sha1,
private ?IRNGProvider $rngprovider = null,
private ?ITimeProvider $timeprovider = null
) {
if ($this->digits <= 0) {
throw new TwoFactorAuthException('Digits must be > 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);
@@ -47,203 +51,161 @@ class TwoFactorAuth
/**
* Create a new secret
*/
public function createSecret($bits = 80, $requirecryptosecure = true)
public function createSecret(int $bits = 160): string
{
$secret = '';
$bytes = 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');
$bytes = (int)ceil($bits / 5); // We use 5 bits of each byte (since we have a 32-character 'alphabet' / BASE32)
$rngprovider = $this->getRngProvider();
$rnd = $rngprovider->getRandomBytes($bytes);
for ($i = 0; $i < $bytes; $i++)
for ($i = 0; $i < $bytes; $i++) {
$secret .= self::$_base32[ord($rnd[$i]) & 31]; //Mask out left 3 bits for 0-31 values
}
return $secret;
}
/**
* Calculate the code with given secret and point in time
*/
public function getCode($secret, $time = null)
public function getCode(#[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($value % pow(10, $this->digits), $this->digits, '0', STR_PAD_LEFT);
return str_pad((string)($value % 10 ** $this->digits), $this->digits, '0', STR_PAD_LEFT);
}
/**
* Check if the code is correct. This will accept codes starting from ($discrepancy * $period) sec ago to ($discrepancy * period) sec from now
*/
public function verifyCode($secret, $code, $discrepancy = 1, $time = null)
public function verifyCode(string $secret, string $code, int $discrepancy = 1, ?int $time = null, ?int &$timeslice = 0): bool
{
$result = false;
$timetamp = $this->getTime($time);
$timestamp = $this->getTime($time);
// To keep safe from timing-attachs we iterate *all* possible codes even though we already may have verified a code is correct
for ($i = -$discrepancy; $i <= $discrepancy; $i++)
$result |= $this->codeEquals($this->getCode($secret, $timetamp + ($i * $this->period)), $code);
$timeslice = 0;
return (bool)$result;
}
/**
* Timing-attack safe comparison of 2 codes (see http://blog.ircmaxell.com/2014/11/its-all-about-time.html)
*/
private function codeEquals($safe, $user) {
if (function_exists('hash_equals')) {
return hash_equals($safe, $user);
// To keep safe from timing-attacks we iterate *all* possible codes even though we already may have
// verified a code is correct. We use the timeslice variable to hold either 0 (no match) or the timeslice
// of the match. Each iteration we either set the timeslice variable to the timeslice of the match
// or set the value to itself. This is an effort to maintain constant execution time for the code.
for ($i = -$discrepancy; $i <= $discrepancy; $i++) {
$ts = $timestamp + ($i * $this->period);
$slice = $this->getTimeSlice($ts);
$timeslice = hash_equals($this->getCode($secret, $ts), $code) ? $slice : $timeslice;
}
// 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;
return $timeslice > 0;
}
/**
* Get data-uri of QRCode
*/
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<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 && !is_array($timeproviders))
throw new TwoFactorAuthException('No timeproviders specified');
if ($timeproviders == null)
if ($timeproviders === null) {
$timeproviders = array(
new Providers\Time\ConvertUnixTimeDotComTimeProvider(),
new Providers\Time\HttpTimeProvider()
new NTPTimeProvider(),
new HttpTimeProvider(),
);
}
// Get default time provider
$timeprovider = $this->getTimeProvider();
// Iterate specified time providers
foreach ($timeproviders as $t) {
if (!($t instanceof ITimeProvider))
if (!($t instanceof ITimeProvider)) {
throw new TwoFactorAuthException('Object does not implement ITimeProvider');
}
// Get time from default time provider and compare to specific time provider and throw if time difference is more than specified number of seconds leniency
if (abs($timeprovider->getTime() - $t->getTime()) > $leniency)
if (abs($timeprovider->getTime() - $t->getTime()) > $leniency) {
throw new TwoFactorAuthException(sprintf('Time for timeprovider is off by more than %d seconds when compared to %s', $leniency, get_class($t)));
}
}
}
private function getTime($time)
{
return ($time === null) ? $this->getTimeProvider()->getTime() : $time;
}
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
*/
public function getQRText($label, $secret)
public function getQRText(string $label, #[SensitiveParameter] string $secret): string
{
return 'otpauth://totp/' . rawurlencode($label)
. '?secret=' . rawurlencode($secret)
. '&issuer=' . rawurlencode($this->issuer)
. '&period=' . intval($this->period)
. '&algorithm=' . rawurlencode(strtoupper($this->algorithm))
. '&digits=' . intval($this->digits);
. '&issuer=' . rawurlencode((string)$this->issuer)
. '&period=' . $this->period
. '&algorithm=' . rawurlencode(strtoupper($this->algorithm->value))
. '&digits=' . $this->digits;
}
private function base32Decode($value)
/**
* @throws TwoFactorAuthException
*/
public function getRngProvider(): IRNGProvider
{
if (strlen($value)==0) return '';
return $this->rngprovider ??= new CSRNGProvider();
}
if (preg_match('/[^'.preg_quote(self::$_base32dict).']/', $value) !== 0)
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) {
throw new TwoFactorAuthException('Invalid base32 string');
}
$buffer = '';
foreach (str_split($value) as $char)
{
if ($char !== '=')
$buffer .= str_pad(decbin(self::$_base32lookup[$char]), 5, 0, STR_PAD_LEFT);
foreach (str_split($value) as $char) {
if ($char !== '=') {
$buffer .= str_pad(decbin(self::$_base32lookup[$char]), 5, '0', STR_PAD_LEFT);
}
}
$length = strlen($buffer);
$blocks = trim(chunk_split(substr($buffer, 0, $length - ($length % 8)), 8, ' '));
$output = '';
foreach (explode(' ', $blocks) as $block)
$output .= chr(bindec(str_pad($block, 8, 0, STR_PAD_RIGHT)));
foreach (explode(' ', $blocks) as $block) {
$output .= chr(bindec(str_pad($block, 8, '0', STR_PAD_RIGHT)));
}
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 Providers\Qr\GoogleQRCodeProvider();
}
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 Providers\Rng\CSRNGProvider();
}
if (function_exists('mcrypt_create_iv')) {
return $this->rngprovider = new Providers\Rng\MCryptRNGProvider();
}
if (function_exists('openssl_random_pseudo_bytes')) {
return $this->rngprovider = new Providers\Rng\OpenSSLRNGProvider();
}
if (function_exists('hash')) {
return $this->rngprovider = new Providers\Rng\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 Providers\Time\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
+27
View File
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertDeprecationsToExceptions="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
stopOnFailure="false">
<testsuites>
<testsuite name="Unit">
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">./lib</directory>
</include>
<report>
<html outputDirectory="build/coverage"/>
<text outputFile="php://stdout"/>
</report>
</coverage>
</phpunit>
@@ -0,0 +1,56 @@
<?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;
class IQRCodeProviderTest extends TestCase
{
use HandlesDataUri;
protected IQRCodeProvider $qr;
protected function setUp(): void
{
$this->qr = new TestQrProvider();
}
public function testTotpUriIsCorrect(): void
{
$tfa = new TwoFactorAuth($this->qr, 'Test&Issuer', 6, 30, Algorithm::Sha1);
$data = $this->DecodeDataUri($tfa->getQRCodeImageAsDataUri('Test&Label', 'VMR466AB62ZBOKHE'));
$this->assertSame('test/test', $data['mimetype']);
$this->assertSame('base64', $data['encoding']);
$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($this->qr, null, 6, 30, Algorithm::Sha1);
$data = $this->DecodeDataUri($tfa->getQRCodeImageAsDataUri('Test&Label', 'VMR466AB62ZBOKHE'));
$this->assertSame('test/test', $data['mimetype']);
$this->assertSame('base64', $data['encoding']);
$this->assertSame('otpauth://totp/Test%26Label?secret=VMR466AB62ZBOKHE&issuer=&period=30&algorithm=SHA1&digits=6@200', $data['data']);
}
public function testGetQRCodeImageAsDataUriThrowsOnInvalidSize(): void
{
$tfa = new TwoFactorAuth($this->qr, 'Test', 6, 30, Algorithm::Sha1);
$this->expectException(TwoFactorAuthException::class);
$tfa->getQRCodeImageAsDataUri('Test', 'VMR466AB62ZBOKHE', 0);
}
}
+20
View File
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Tests\Providers\Qr;
use RobThree\Auth\Providers\Qr\IQRCodeProvider;
class TestQrProvider implements IQRCodeProvider
{
public function getQRCodeImage(string $qrText, int $size): string
{
return $qrText . '@' . $size;
}
public function getMimeType(): string
{
return 'test/test';
}
}
+21
View File
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Tests\Providers\Rng;
use PHPUnit\Framework\TestCase;
use RobThree\Auth\Providers\Rng\CSRNGProvider;
class CSRNGProviderTest extends TestCase
{
use NeedsRngLengths;
public function testCSRNGProvidersReturnExpectedNumberOfBytes(): void
{
$rng = new CSRNGProvider();
foreach ($this->rngTestLengths as $l) {
$this->assertSame($l, strlen($rng->getRandomBytes($l)));
}
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Tests\Providers\Rng;
use PHPUnit\Framework\TestCase;
use RobThree\Auth\Algorithm;
use RobThree\Auth\TwoFactorAuth;
use Tests\Providers\Qr\TestQrProvider;
class IRNGProviderTest extends TestCase
{
public function testCreateSecret(): void
{
$tfa = new TwoFactorAuth(new TestQrProvider(), 'Test', 6, 30, Algorithm::Sha1, null, null);
$this->assertIsString($tfa->createSecret());
}
}
+11
View File
@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Tests\Providers\Rng;
trait NeedsRngLengths
{
/** @var array<int> */
protected $rngTestLengths = array(1, 16, 32, 256);
}
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Tests\Providers\Time;
use PHPUnit\Framework\TestCase;
use RobThree\Auth\Algorithm;
use RobThree\Auth\TwoFactorAuth;
use RobThree\Auth\TwoFactorAuthException;
use Tests\Providers\Qr\TestQrProvider;
class ITimeProviderTest extends TestCase
{
public function testEnsureCorrectTimeDoesNotThrowForCorrectTime(): void
{
$this->expectNotToPerformAssertions();
$tpr1 = new TestTimeProvider(123);
$tpr2 = new TestTimeProvider(128);
$tfa = new TwoFactorAuth(new TestQrProvider(), 'Test', 6, 30, Algorithm::Sha1, null, $tpr1);
$tfa->ensureCorrectTime(array($tpr2)); // 128 - 123 = 5 => within default leniency
}
public function testEnsureCorrectTimeThrowsOnIncorrectTime(): void
{
$tpr1 = new TestTimeProvider(123);
$tpr2 = new TestTimeProvider(124);
$tfa = new TwoFactorAuth(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
}
public function testEnsureDefaultTimeProviderReturnsCorrectTime(): void
{
$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
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Tests\Providers\Time;
use RobThree\Auth\Providers\Time\ITimeProvider;
class TestTimeProvider implements ITimeProvider
{
/** @var int */
private $time;
/**
* @param int $time
*/
public function __construct($time)
{
$this->time = $time;
}
/**
* {@inheritdoc}
*/
public function getTime()
{
return $this->time;
}
}
+128 -309
View File
@@ -1,192 +1,119 @@
<?php
require_once 'lib/TwoFactorAuth.php';
require_once 'lib/TwoFactorAuthException.php';
require_once 'lib/Providers/Qr/IQRCodeProvider.php';
require_once 'lib/Providers/Qr/BaseHTTPQRCodeProvider.php';
require_once 'lib/Providers/Qr/GoogleQRCodeProvider.php';
require_once 'lib/Providers/Qr/QRException.php';
declare(strict_types=1);
require_once 'lib/Providers/Rng/IRNGProvider.php';
require_once 'lib/Providers/Rng/RNGException.php';
require_once 'lib/Providers/Rng/CSRNGProvider.php';
require_once 'lib/Providers/Rng/MCryptRNGProvider.php';
require_once 'lib/Providers/Rng/OpenSSLRNGProvider.php';
require_once 'lib/Providers/Rng/HashRNGProvider.php';
require_once 'lib/Providers/Rng/RNGException.php';
require_once 'lib/Providers/Time/ITimeProvider.php';
require_once 'lib/Providers/Time/LocalMachineTimeProvider.php';
require_once 'lib/Providers/Time/HttpTimeProvider.php';
require_once 'lib/Providers/Time/ConvertUnixTimeDotComTimeProvider.php';
require_once 'lib/Providers/Time/TimeException.php';
namespace Tests;
use PHPUnit\Framework\TestCase;
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\Providers\Qr\IQRCodeProvider;
use RobThree\Auth\Providers\Rng\IRNGProvider;
use RobThree\Auth\Providers\Time\ITimeProvider;
use RobThree\Auth\TwoFactorAuthException;
use Tests\Providers\Qr\TestQrProvider;
class TwoFactorAuthTest extends PHPUnit_Framework_TestCase
class TwoFactorAuthTest extends TestCase
{
/**
* @expectedException \RobThree\Auth\TwoFactorAuthException
*/
public function testConstructorThrowsOnInvalidDigits() {
public function testConstructorThrowsOnInvalidDigits(): void
{
$this->expectException(TwoFactorAuthException::class);
new TwoFactorAuth('Test', 0);
new TwoFactorAuth(new TestQrProvider(), 'Test', 0);
}
/**
* @expectedException \RobThree\Auth\TwoFactorAuthException
*/
public function testConstructorThrowsOnInvalidPeriod() {
public function testConstructorThrowsOnInvalidPeriod(): void
{
$this->expectException(TwoFactorAuthException::class);
new TwoFactorAuth('Test', 6, 0);
new TwoFactorAuth(new TestQrProvider(), 'Test', 6, 0);
}
/**
* @expectedException \RobThree\Auth\TwoFactorAuthException
*/
public function testConstructorThrowsOnInvalidAlgorithm() {
new TwoFactorAuth('Test', 6, 30, 'xxx');
public function testGetCodeReturnsCorrectResults(): void
{
$tfa = new TwoFactorAuth(new TestQrProvider(), 'Test');
$this->assertSame('543160', $tfa->getCode('VMR466AB62ZBOKHE', 1426847216));
$this->assertSame('538532', $tfa->getCode('VMR466AB62ZBOKHE', 0));
}
public function testGetCodeReturnsCorrectResults() {
$tfa = new TwoFactorAuth('Test');
$this->assertEquals('543160', $tfa->getCode('VMR466AB62ZBOKHE', 1426847216));
$this->assertEquals('538532', $tfa->getCode('VMR466AB62ZBOKHE', 0));
}
/**
* @expectedException \RobThree\Auth\TwoFactorAuthException
*/
public function testCreateSecretThrowsOnInsecureRNGProvider() {
$rng = new TestRNGProvider();
$tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, $rng);
$tfa->createSecret();
}
public function testCreateSecretOverrideSecureDoesNotThrowOnInsecureRNG() {
$rng = new TestRNGProvider();
$tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, $rng);
$this->assertEquals('ABCDEFGHIJKLMNOP', $tfa->createSecret(80, false));
}
public function testCreateSecretDoesNotThrowOnSecureRNGProvider() {
$rng = new TestRNGProvider(true);
$tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, $rng);
$this->assertEquals('ABCDEFGHIJKLMNOP', $tfa->createSecret());
}
public function testCreateSecretGeneratesDesiredAmountOfEntropy() {
$rng = new TestRNGProvider(true);
$tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, $rng);
$this->assertEquals('A', $tfa->createSecret(5));
$this->assertEquals('AB', $tfa->createSecret(6));
$this->assertEquals('ABCDEFGHIJKLMNOPQRSTUVWXYZ', $tfa->createSecret(128));
$this->assertEquals('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', $tfa->createSecret(160));
$this->assertEquals('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', $tfa->createSecret(320));
$this->assertEquals('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUVWXYZ234567A', $tfa->createSecret(321));
}
public function testEnsureCorrectTimeDoesNotThrowForCorrectTime() {
$tpr1 = new TestTimeProvider(123);
$tpr2 = new TestTimeProvider(128);
$tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, null, $tpr1);
$tfa->ensureCorrectTime(array($tpr2)); // 128 - 123 = 5 => within default leniency
}
/**
* @expectedException \RobThree\Auth\TwoFactorAuthException
*/
public function testEnsureCorrectTimeThrowsOnIncorrectTime() {
$tpr1 = new TestTimeProvider(123);
$tpr2 = new TestTimeProvider(124);
$tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, null, $tpr1);
$tfa->ensureCorrectTime(array($tpr2), 0); // We force a leniency of 0, 124-123 = 1 so this should throw
}
public function testEnsureDefaultTimeProviderReturnsCorrectTime() {
$tfa = new TwoFactorAuth('Test', 6, 30, 'sha1');
$tfa->ensureCorrectTime(array(new TestTimeProvider(time())), 1); // Use a leniency of 1, should the time change between both time() calls
}
public function testEnsureAllTimeProvidersReturnCorrectTime() {
$tfa = new TwoFactorAuth('Test', 6, 30, 'sha1');
public function testEnsureAllTimeProvidersReturnCorrectTime(): void
{
$tfa = new TwoFactorAuth(new TestQrProvider(), 'Test', 6, 30, Algorithm::Sha1);
$tfa->ensureCorrectTime(array(
new RobThree\Auth\Providers\Time\ConvertUnixTimeDotComTimeProvider(),
new RobThree\Auth\Providers\Time\HttpTimeProvider(), // Uses google.com by default
new RobThree\Auth\Providers\Time\HttpTimeProvider('https://github.com'),
new RobThree\Auth\Providers\Time\HttpTimeProvider('https://yahoo.com'),
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 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 HttpTimeProvider('https://yahoo.com'),
));
$this->expectNotToPerformAssertions();
}
public function testVerifyCodeWorksCorrectly() {
public function testVerifyCodeWorksCorrectly(): void
{
$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
$tfa = new TwoFactorAuth('Test', 6, 30);
$this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847190));
$this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 0, 1426847190 + 29)); //Test discrepancy
$this->assertEquals(false, $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 0, 1426847190 + 30)); //Test discrepancy
$this->assertEquals(false, $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 0, 1426847190 - 1)); //Test discrepancy
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 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->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 + 0)); //Test discrepancy
$this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 + 35)); //Test discrepancy
$this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 - 35)); //Test discrepancy
$this->assertFalse($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 + 65)); //Test discrepancy
$this->assertFalse($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 - 65)); //Test discrepancy
$this->assertEquals(false, $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 + 65)); //Test discrepancy
$this->assertEquals(false, $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 - 65)); //Test discrepancy
$this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 2, 1426847205 + 65)); //Test discrepancy
$this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 2, 1426847205 - 65)); //Test discrepancy
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 2, 1426847205 + 65)); //Test discrepancy
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 2, 1426847205 - 65)); //Test discrepancy
}
public function testTotpUriIsCorrect() {
$qr = new TestQrProvider();
public function testVerifyCorrectTimeSliceIsReturned(): void
{
$tfa = new TwoFactorAuth(new TestQrProvider(), 'Test', 6, 30);
$tfa = new TwoFactorAuth('Test&Issuer', 6, 30, 'sha1', $qr);
$data = $this->DecodeDataUri($tfa->getQRCodeImageAsDataUri('Test&Label', 'VMR466AB62ZBOKHE'));
$this->assertEquals('test/test', $data['mimetype']);
$this->assertEquals('base64', $data['encoding']);
$this->assertEquals('otpauth://totp/Test%26Label?secret=VMR466AB62ZBOKHE&issuer=Test%26Issuer&period=30&algorithm=SHA1&digits=6@200', $data['data']);
// We test with discrepancy 3 (so total of 7 codes: c-3, c-2, c-1, c, c+1, c+2, c+3
// Ensure each corresponding timeslice is returned correctly
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '534113', 3, 1426847190, $timeslice1));
$this->assertSame(47561570, $timeslice1);
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '819652', 3, 1426847190, $timeslice2));
$this->assertSame(47561571, $timeslice2);
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '915954', 3, 1426847190, $timeslice3));
$this->assertSame(47561572, $timeslice3);
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 3, 1426847190, $timeslice4));
$this->assertSame(47561573, $timeslice4);
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '348401', 3, 1426847190, $timeslice5));
$this->assertSame(47561574, $timeslice5);
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '648525', 3, 1426847190, $timeslice6));
$this->assertSame(47561575, $timeslice6);
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '170645', 3, 1426847190, $timeslice7));
$this->assertSame(47561576, $timeslice7);
// Incorrect code should return false and a 0 timeslice
$this->assertFalse($tfa->verifyCode('VMR466AB62ZBOKHE', '111111', 3, 1426847190, $timeslice8));
$this->assertSame(0, $timeslice8);
}
/**
* @expectedException \RobThree\Auth\TwoFactorAuthException
*/
public function testGetQRCodeImageAsDataUriThrowsOnInvalidSize() {
$qr = new TestQrProvider();
public function testGetCodeThrowsOnInvalidBase32String1(): void
{
$tfa = new TwoFactorAuth(new TestQrProvider(), 'Test');
$tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', $qr);
$tfa->getQRCodeImageAsDataUri('Test', 'VMR466AB62ZBOKHE', 0);
}
$this->expectException(TwoFactorAuthException::class);
/**
* @expectedException \RobThree\Auth\TwoFactorAuthException
*/
public function testGetCodeThrowsOnInvalidBase32String1() {
$tfa = new TwoFactorAuth('Test');
$tfa->getCode('FOO1BAR8BAZ9'); //1, 8 & 9 are invalid chars
}
/**
* @expectedException \RobThree\Auth\TwoFactorAuthException
*/
public function testGetCodeThrowsOnInvalidBase32String2() {
$tfa = new TwoFactorAuth('Test');
public function testGetCodeThrowsOnInvalidBase32String2(): void
{
$tfa = new TwoFactorAuth(new TestQrProvider(), 'Test');
$this->expectException(TwoFactorAuthException::class);
$tfa->getCode('mzxw6==='); //Lowercase
}
public function testKnownBase32DecodeTestVectors() {
public function testKnownBase32DecodeTestVectors(): void
{
// We usually don't test internals (e.g. privates) but since we rely heavily on base32 decoding and don't want
// to expose this method nor do we want to give people the possibility of implementing / providing their own base32
// decoding/decoder (as we do with Rng/QR providers for example) we simply test the private base32Decode() method
@@ -198,184 +125,76 @@ class TwoFactorAuthTest extends PHPUnit_Framework_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('RobThree\Auth\TwoFactorAuth', '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======'));
}
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('RobThree\Auth\TwoFactorAuth', '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'));
}
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));
}
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));
}
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));
}
/**
* @requires function random_bytes
*/
public function testCSRNGProvidersReturnExpectedNumberOfBytes() {
$rng = new \RobThree\Auth\Providers\Rng\CSRNGProvider();
foreach ($this->getRngTestLengths() as $l)
$this->assertEquals($l, strlen($rng->getRandomBytes($l)));
$this->assertEquals(true, $rng->isCryptographicallySecure());
}
/**
* @requires function hash_algos
* @requires function hash
*/
public function testHashRNGProvidersReturnExpectedNumberOfBytes() {
$rng = new \RobThree\Auth\Providers\Rng\HashRNGProvider();
foreach ($this->getRngTestLengths() as $l)
$this->assertEquals($l, strlen($rng->getRandomBytes($l)));
$this->assertEquals(false, $rng->isCryptographicallySecure());
}
/**
* @requires function mcrypt_create_iv
*/
public function testMCryptRNGProvidersReturnExpectedNumberOfBytes() {
$rng = new \RobThree\Auth\Providers\Rng\MCryptRNGProvider();
foreach ($this->getRngTestLengths() as $l)
$this->assertEquals($l, strlen($rng->getRandomBytes($l)));
$this->assertEquals(true, $rng->isCryptographicallySecure());
}
/**
* @requires function openssl_random_pseudo_bytes
*/
public function testStrongOpenSSLRNGProvidersReturnExpectedNumberOfBytes() {
$rng = new \RobThree\Auth\Providers\Rng\OpenSSLRNGProvider(true);
foreach ($this->getRngTestLengths() as $l)
$this->assertEquals($l, strlen($rng->getRandomBytes($l)));
$this->assertEquals(true, $rng->isCryptographicallySecure());
}
/**
* @requires function openssl_random_pseudo_bytes
*/
public function testNonStrongOpenSSLRNGProvidersReturnExpectedNumberOfBytes() {
$rng = new \RobThree\Auth\Providers\Rng\OpenSSLRNGProvider(false);
foreach ($this->getRngTestLengths() as $l)
$this->assertEquals($l, strlen($rng->getRandomBytes($l)));
$this->assertEquals(false, $rng->isCryptographicallySecure());
}
private function getRngTestLengths() {
return array(1, 16, 32, 256);
}
private function DecodeDataUri($datauri) {
if (preg_match('/data:(?P<mimetype>[\w\.\-\/]+);(?P<encoding>\w+),(?P<data>.*)/', $datauri, $m) === 1) {
return array(
'mimetype' => $m['mimetype'],
'encoding' => $m['encoding'],
'data' => base64_decode($m['data'])
);
}
return null;
$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));
}
}
class TestRNGProvider implements IRNGProvider {
private $isSecure;
function __construct($isSecure = false) {
$this->isSecure = $isSecure;
}
public function getRandomBytes($bytecount) {
$result = '';
for ($i=0; $i<$bytecount; $i++)
$result.=chr($i);
return $result;
}
public function isCryptographicallySecure() {
return $this->isSecure;
}
}
class TestQrProvider implements IQRCodeProvider {
public function getQRCodeImage($qrtext, $size) {
return $qrtext . '@' . $size;
}
public function getMimeType() {
return 'test/test';
}
}
class TestTimeProvider implements ITimeProvider {
private $time;
function __construct($time) {
$this->time = $time;
}
public function getTime() {
return $this->time;
}
}
+62
View File
@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace TestsDependency;
use PHPUnit\Framework\TestCase;
use RobThree\Auth\Algorithm;
use RobThree\Auth\Providers\Qr\BaconQrCodeProvider;
use RobThree\Auth\Providers\Qr\HandlesDataUri;
use RobThree\Auth\Providers\Qr\IQRCodeProvider;
use RobThree\Auth\TwoFactorAuth;
use RuntimeException;
class BaconQRCodeTest extends TestCase
{
use HandlesDataUri;
protected IQRCodeProvider $qr;
protected function setUp(): void
{
$this->qr = new BaconQrCodeProvider(1, '#000', '#FFF', 'svg');
;
}
public function testDependency(): void
{
$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(): void
{
$this->expectException(RuntimeException::class);
new BaconQrCodeProvider(1, '#000', 'not-a-colour');
}
public function testBadTextColourHexRef(): void
{
$this->expectException(RuntimeException::class);
new BaconQrCodeProvider(1, '#AAAA', '#FFF');
}
public function testBadBackgroundColourHexRef(): void
{
$this->expectException(RuntimeException::class);
new BaconQrCodeProvider(1, '#000', '#AAAA');
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace TestsDependency;
use PHPUnit\Framework\TestCase;
use RobThree\Auth\Algorithm;
use RobThree\Auth\Providers\Qr\EndroidQrCodeProvider;
use RobThree\Auth\Providers\Qr\HandlesDataUri;
use RobThree\Auth\TwoFactorAuth;
class EndroidQRCodeTest extends TestCase
{
use HandlesDataUri;
public function testDependency(): void
{
$qr = new EndroidQrCodeProvider();
$tfa = new TwoFactorAuth($qr, 'Test&Issuer', 6, 30, Algorithm::Sha1);
$data = $this->DecodeDataUri($tfa->getQRCodeImageAsDataUri('Test&Label', 'VMR466AB62ZBOKHE'));
$this->assertSame('image/png', $data['mimetype']);
$this->assertSame('base64', $data['encoding']);
$this->assertNotEmpty($data['data']);
}
}