├── .github
└── workflows
│ └── run-tests.yml
├── .gitignore
├── .scrutinizer.yml
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── composer.json
├── docs
└── middleware.jpg
├── phpspec.yml
├── phpunit.xml
├── src
├── Events
│ ├── EmptyOneTimePasswordReceived.php
│ ├── LoggedOut.php
│ ├── LoginFailed.php
│ ├── LoginSucceeded.php
│ ├── OneTimePasswordExpired.php
│ ├── OneTimePasswordRequested.php
│ └── OneTimePasswordRequested53.php
├── Exceptions
│ ├── InvalidOneTimePassword.php
│ └── InvalidSecretKey.php
├── Facade.php
├── Google2FA.php
├── Listeners
│ └── LoginViaRemember.php
├── Middleware.php
├── MiddlewareStateless.php
├── ServiceProvider.php
├── Support
│ ├── Auth.php
│ ├── Authenticator.php
│ ├── Config.php
│ ├── Constants.php
│ ├── ErrorBag.php
│ ├── Input.php
│ ├── Request.php
│ ├── Response.php
│ └── Session.php
└── config
│ ├── .gitkeep
│ └── config.php
├── tests
├── .gitkeep
├── Constants.php
├── Google2FaLaravelTest.php
├── Support
│ └── User.php
├── TestCase.php
├── bootstrap.php
└── views
│ └── google2fa
│ └── index.blade.php
└── upgrading.md
/.github/workflows/run-tests.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on:
4 | push:
5 | paths:
6 | - '.github/workflows/run-tests.yml'
7 | - 'composer.json'
8 | - 'phpunit.xml'
9 | - 'src/**'
10 | - 'tests/**'
11 | pull_request:
12 | paths:
13 | - '.github/workflows/run-tests.yml'
14 | - 'composer.json'
15 | - 'phpunit.xml'
16 | - 'src/**'
17 | - 'tests/**'
18 | schedule:
19 | - cron: '0 0 * * *'
20 |
21 | jobs:
22 | php-tests:
23 | runs-on: ubuntu-22.04
24 | timeout-minutes: 15
25 | env:
26 | COMPOSER_NO_INTERACTION: 1
27 |
28 | strategy:
29 | fail-fast: false
30 | matrix:
31 | php: [
32 | 8.4,
33 | 8.3,
34 | 8.2,
35 | 8.1,
36 | 8.0,
37 | 7.4,
38 | 7.3,
39 | 7.2,
40 | ]
41 | laravel: [
42 | ^12,
43 | ^11,
44 | ^10,
45 | ^9,
46 | ^8,
47 | ^7,
48 | ^6,
49 | ^5.4.36,
50 | ]
51 | exclude:
52 | - php: 8.4
53 | laravel: ^7
54 | - php: 8.4
55 | laravel: ^6
56 | - php: 8.4
57 | laravel: ^5.4.36
58 | - php: 8.3
59 | laravel: ^7
60 | - php: 8.3
61 | laravel: ^6
62 | - php: 8.3
63 | laravel: ^5.4.36
64 | - php: 8.2
65 | laravel: ^7
66 | - php: 8.2
67 | laravel: ^6
68 | - php: 8.2
69 | laravel: ^5.4.36
70 | - php: 8.1
71 | laravel: ^12
72 | - php: 8.1
73 | laravel: ^11
74 | - php: 8.1
75 | laravel: ^7
76 | - php: 8.1
77 | laravel: ^6
78 | - php: 8.1
79 | laravel: ^5.4.36
80 | - php: 8.0
81 | laravel: ^12
82 | - php: 8.0
83 | laravel: ^11
84 | - php: 8.0
85 | laravel: ^10
86 | - php: 8.0
87 | laravel: ^5.4.36
88 | - php: 7.4
89 | laravel: ^12
90 | - php: 7.4
91 | laravel: ^11
92 | - php: 7.4
93 | laravel: ^10
94 | - php: 7.4
95 | laravel: ^9
96 | - php: 7.3
97 | laravel: ^12
98 | - php: 7.3
99 | laravel: ^11
100 | - php: 7.3
101 | laravel: ^10
102 | - php: 7.3
103 | laravel: ^9
104 | - php: 7.2
105 | laravel: ^12
106 | - php: 7.2
107 | laravel: ^11
108 | - php: 7.2
109 | laravel: ^10
110 | - php: 7.2
111 | laravel: ^9
112 | - php: 7.2
113 | laravel: ^8
114 |
115 | name: P${{ matrix.php }} L${{ matrix.laravel }}
116 |
117 | steps:
118 | - name: Checkout code
119 | uses: actions/checkout@v4
120 |
121 | - name: Setup PHP
122 | uses: shivammathur/setup-php@v2
123 | with:
124 | php-version: ${{ matrix.php }}
125 | coverage: none
126 | extensions: pdo_sqlite, fileinfo
127 |
128 | - run: composer require laravel/framework:${{ matrix.laravel }} --no-update
129 |
130 | - name: Get composer cache directory
131 | id: composer-cache
132 | run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
133 |
134 | - name: Cache dependencies
135 | uses: actions/cache@v4
136 | with:
137 | path: ${{ steps.composer-cache.outputs.dir }}
138 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
139 | restore-keys: ${{ runner.os }}-composer-
140 |
141 | - name: Install dependencies
142 | run: composer update --prefer-dist --no-plugins
143 |
144 | - name: phpunit
145 | run: vendor/bin/phpunit
146 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | composer.phar
3 | composer.lock
4 | vendor
5 | coverage/*
6 | .phpunit.result.cache
7 |
--------------------------------------------------------------------------------
/.scrutinizer.yml:
--------------------------------------------------------------------------------
1 | checks:
2 | php:
3 | remove_extra_empty_lines: true
4 | remove_php_closing_tag: true
5 | remove_trailing_whitespace: true
6 | fix_use_statements:
7 | remove_unused: true
8 | preserve_multiple: false
9 | preserve_blanklines: true
10 | order_alphabetically: true
11 | fix_php_opening_tag: true
12 | fix_linefeed: true
13 | fix_line_ending: true
14 | fix_identation_4spaces: true
15 | fix_doc_comments: true
16 |
17 | filter:
18 | paths: [src/*]
19 | excluded_paths: ["tests/*"]
20 |
21 | coding_style:
22 | php: { }
23 |
24 | tools:
25 | external_code_coverage: true
26 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 | php:
4 | - 7.1
5 | - 7.2
6 | - 7.3
7 | - 7.4
8 | # - nightly
9 |
10 | ## Cache composer
11 | cache:
12 | directories:
13 | - $HOME/.composer/cache
14 |
15 | env:
16 | - LARAVEL_VERSION=5.8.* TESTBENCH_VERSION=3.8.*
17 | - LARAVEL_VERSION=6.* TESTBENCH_VERSION=4.*
18 | - LARAVEL_VERSION=7.* TESTBENCH_VERSION=5.*
19 | - LARAVEL_VERSION=8.* TESTBENCH_VERSION=6.*
20 |
21 | matrix:
22 | exclude:
23 | - php: 7.1
24 | env: LARAVEL_VERSION=6.* TESTBENCH_VERSION=4.*
25 | - php: 7.1
26 | env: LARAVEL_VERSION=7.* TESTBENCH_VERSION=5.*
27 | - php: 7.1
28 | env: LARAVEL_VERSION=8.* TESTBENCH_VERSION=6.*
29 | - php: 7.2
30 | env: LARAVEL_VERSION=8.* TESTBENCH_VERSION=6.*
31 |
32 | before_script:
33 | - yes '' | pecl install imagick
34 | - composer require "laravel/framework:${LARAVEL_VERSION}" "orchestra/testbench:${TESTBENCH_VERSION}" --no-update
35 | - travis_retry composer update --no-interaction --prefer-dist
36 |
37 | script:
38 | - vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover
39 |
40 | after_script:
41 | - |
42 | if [[ "$TRAVIS_PHP_VERSION" == '7.4' ]]; then
43 | wget https://scrutinizer-ci.com/ocular.phar
44 | php ocular.phar code-coverage:upload --format=php-clover coverage.clover
45 | fi
46 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## Change Log
2 |
3 | ## [2.0.1] - 2021-08-15
4 | ### Added
5 | - Make image backend optional if bacon is not installed
6 |
7 | ## [2.0.0] - 2021-08-15
8 | ### Added
9 | - Allow Google2FA QRCode 3.0
10 |
11 | ## [1.4.1] - 2020-09-20
12 | ### Added
13 | - Improve PHP 8 compatibility by not defaulting to imagemagick
14 | - Make the code PHP 7.0 compatible
15 | - Supports using a different guard
16 | - Ability to setWindow dynamically
17 |
18 | ## [1.4.0] - 2020-09-20
19 | ### Added
20 | - Laravel 8 support
21 |
22 | ## [1.1.2] - 2019-03-13
23 | ### Added
24 | - New config item for empty OTP error messages: 'cannot_be_empty' => 'One Time Password cannot be empty.',
25 | - Tests to check for HTML error messages
26 |
27 | ## [1.1.1] - 2019-03-12
28 | ### Removed
29 | - PHP 7.0 support
30 |
31 | ## [1.1.0] - 2019-09-10
32 | ### Added
33 | - Laravel 6 support
34 |
35 | ## [1.0.0] - 2019-03-21
36 | - Start using Google2FA QRCode as base class
37 | - Support QRCode generation again
38 | - Support API / Stateless requests
39 |
40 | ## [0.3.0] - 2019-03-20
41 | - Upgrade to GoogleFA 5.0
42 |
43 | ## [0.2.1] - 2019-03-20
44 | - Removed QRCode generation via Google2FA 4.0
45 |
46 | ## [0.2.0] - 2018-03-07
47 | ### Add
48 | - Firing events:
49 | - EmptyOneTimePasswordReceived
50 | - LoggedOut
51 | - LoginFailed
52 | - LoginSucceeded
53 | - OneTimePasswordExpired
54 | - OneTimePasswordRequested
55 | ### Changed
56 | - Google2FA is not dependent from Middleware anymore
57 | - Small refactor
58 | - Change license to MIT
59 | - Move from phpspec tests to PHPUnit and Orchestra
60 | ### Removed
61 | - Support for PHP 5.4-5.6 (should still work, but would have to be tested by users)
62 |
63 | ## [0.1.4] - 2017-12-05
64 | ### Add
65 | - Support Laravel 5.2+
66 |
67 | ## [0.1.2] - 2016-06-22
68 | ### Fixed
69 | - Fix middleware returning nulls
70 |
71 | ## [0.1.1] - 2016-06-22
72 | ### Fixed
73 | - Keepalive time not being updated correctly
74 |
75 | ## [0.1.0] - 2016-06-21
76 | ### Added
77 | - First version
78 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) Antonio Carlos Ribeiro
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Google2FA for Laravel
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | ### Google Two-Factor Authentication Package for Laravel
17 |
18 | Google2FA is a PHP implementation of the Google Two-Factor Authentication Module, supporting the HMAC-Based One-time Password (HOTP) algorithm specified in [RFC 4226](https://tools.ietf.org/html/rfc4226) and the Time-based One-time Password (TOTP) algorithm specified in [RFC 6238](https://tools.ietf.org/html/rfc6238).
19 |
20 | This package is a Laravel bridge to [Google2FA](https://github.com/antonioribeiro/google2fa)'s PHP package.
21 |
22 | The intent of this package is to create QRCodes for Google2FA and check user typed codes. If you need to create backup/recovery codes, please check below.
23 |
24 | ### Recovery/Backup codes
25 |
26 | if you need to create recovery or backup codes to provide a way for your users to recover a lost account, you can use the [Recovery Package](https://github.com/antonioribeiro/recovery).
27 |
28 | ## Demos, Example & Playground
29 |
30 | Please check the [Google2FA Package Playground](https://pragmarx.com/playground/google2fa#/).
31 |
32 | 
33 |
34 | Here's an demo app showing how to use Google2FA: [google2fa-example](https://github.com/antonioribeiro/google2fa-example).
35 |
36 | You can scan the QR code on [this (old) demo page](https://antoniocarlosribeiro.com/technology/google2fa) with a Google Authenticator app and view the code changing (almost) in real time.
37 |
38 | ## Compatibility
39 |
40 | | Laravel | [Google2FA](https://github.com/antonioribeiro/google2fa) | Google2FA-Laravel |
41 | |---------|-----------|-------------------|
42 | | 4.2 | <= 1.0.1 | |
43 | | 5.0-5.1 | <= 1.0.1 | |
44 | | 5.2-10.x | >= 2.0.0 | >= 0.2.0 |
45 |
46 | Before Google2FA 2.0 (Laravel 5.1) you have to install `pragmarx/google2fa:~1.0`, because this package was both a Laravel package and a PHP (agnostic).
47 |
48 | ## Demo
49 |
50 | Click [here](https://pragmarx.com/playground/google2fa/middleware) to see the middleware demo:
51 |
52 | 
53 |
54 | ## Installing
55 |
56 | Use Composer to install it:
57 |
58 | composer require pragmarx/google2fa-laravel
59 |
60 |
61 | ## Installing on Laravel
62 |
63 | ### Laravel 5.5 and above
64 |
65 | You don't have to do anything else, this package autoloads the Service Provider and create the Alias, using the new Auto-Discovery feature.
66 |
67 | ### Laravel 5.4 and below
68 |
69 | Add the Service Provider and Facade alias to your `app/config/app.php` (Laravel 4.x) or `config/app.php` (Laravel 5.x):
70 |
71 | ``` php
72 | PragmaRX\Google2FALaravel\ServiceProvider::class,
73 |
74 | 'Google2FA' => PragmaRX\Google2FALaravel\Facade::class,
75 | ```
76 |
77 | ## Publish the config file
78 |
79 | ``` php
80 | php artisan vendor:publish --provider="PragmaRX\Google2FALaravel\ServiceProvider"
81 | ```
82 |
83 | ## Using It
84 |
85 | #### Use the Facade
86 |
87 | ``` php
88 | use Google2FA;
89 |
90 | return Google2FA::generateSecretKey();
91 | ```
92 |
93 | #### In Laravel you can use the IoC Container
94 |
95 | ``` php
96 | $google2fa = app('pragmarx.google2fa');
97 |
98 | return $google2fa->generateSecretKey();
99 | ```
100 |
101 | ## Middleware
102 |
103 | This package has a middleware which will help you code 2FA on your app. To use it, you just have to:
104 |
105 | ### Add the middleware to your Kernel.php:
106 |
107 | ``` php
108 | protected $routeMiddleware = [
109 | ...
110 | '2fa' => \PragmaRX\Google2FALaravel\Middleware::class,
111 | ];
112 | ```
113 |
114 | ### Using it in one or more routes:
115 |
116 | ``` php
117 | Route::get('/admin', function () {
118 | return view('admin.index');
119 | })->middleware(['auth', '2fa']);
120 | ```
121 |
122 | ### QRCode
123 |
124 | This package uses the [Google2FA-QRCode package](https://github.com/antonioribeiro/google2fa-qrcode), please check it for more info on how to configure the proper QRCode generators for your use case.
125 |
126 | ### Imagick QRCode Backend
127 |
128 | There are three available: **imagemagick** (default), **svg** and **eps**.
129 |
130 | You can change it via config:
131 |
132 | ``` php
133 | /*
134 | * Which image backend to use for generating QR codes?
135 | *
136 | * Supports imagemagick, svg and eps
137 | */
138 | 'qrcode_image_backend' => \PragmaRX\Google2FALaravel\Support\Constants::QRCODE_IMAGE_BACKEND_IMAGEMAGICK,
139 | ```
140 |
141 | Or runtime:
142 |
143 | ``` php
144 | Google2FA::setQRCodeBackend('svg');
145 | ```
146 |
147 | ### Configuring the view
148 |
149 | You can set your 'ask for a one time password' view in the config file (config/google2fa.php):
150 |
151 | ``` php
152 | /**
153 | * One Time Password View
154 | */
155 | 'view' => 'google2fa.index',
156 | ```
157 |
158 | And in the view you just have to provide a form containing the input, which is also configurable:
159 |
160 | ``` php
161 | /**
162 | * One Time Password request input name
163 | */
164 | 'otp_input' => 'one_time_password',
165 | ```
166 |
167 | Here's a form example:
168 |
169 | ```html
170 |
175 | ```
176 |
177 | ## One Time Password Lifetime
178 |
179 | Usually an OTP lasts forever, until the user logs off your app, but, to improve application safety, you may want to re-ask, only for the Google OTP, from time to time. So you can set a number of minutes here:
180 |
181 | ``` php
182 | /**
183 | * Lifetime in minutes.
184 | * In case you need your users to be asked for a new one time passwords from time to time.
185 | */
186 |
187 | 'lifetime' => 0, // 0 = eternal
188 | ```
189 | Keep in mind that this uses the Laravel sessions in the background. If this number exceeds the value set in ``config('session.lifetime')`` you will still be logged out, even if your OTP lifetime has not expired.
190 |
191 | And you can decide whether your OTP will be kept alive while your users are browsing the site or not:
192 |
193 | ``` php
194 | /**
195 | * Renew lifetime at every new request.
196 | */
197 |
198 | 'keep_alive' => true,
199 | ```
200 |
201 | ## Manually logging out from 2Fa
202 |
203 | This command will logout your user and redirect he/she to the 2FA form on the next request:
204 |
205 | ``` php
206 | Google2FA::logout();
207 | ```
208 |
209 | If you don't want to use the Facade, you may:
210 |
211 | ``` php
212 | use PragmaRX\Google2FALaravel\Support\Authenticator;
213 |
214 | (new Authenticator(request()))->logout();
215 | ```
216 |
217 | ## Throttling / Lockout after X attempts
218 |
219 | Unless you need something really fancy, you can probably use Laravel's [route throttle middleware](https://laravel.com/docs/6.x/middleware) for that:
220 |
221 | ```php
222 | Route::get('/admin', function () {
223 | return view('admin.index');
224 | })->middleware(['auth', '2fa', 'throttle']);
225 | ```
226 |
227 | ## Stateless usage
228 |
229 | ```php
230 | $authenticator = app(Authenticator::class)->bootStateless($request);
231 |
232 | if ($authenticator->isAuthenticated()) {
233 | // otp auth success!
234 | }
235 | ```
236 |
237 | You can also use a stateless middleware:
238 |
239 | ``` php
240 | protected $routeMiddleware = [
241 | ...
242 | '2fa' => \PragmaRX\Google2FALaravel\MiddlewareStateless::class,
243 | ];
244 | ```
245 |
246 | ## 2FA and Laravel login via remember
247 |
248 | When Laravel login via remember is activated, the session is renovated and the 2FA code is required again. To solve this, add the ``LoginViaRemember`` listener in your ``App\Providers\EventServiceProvider``:
249 |
250 | ``` php
251 | use Illuminate\Auth\Events\Login;
252 | use PragmaRX\Google2FALaravel\Listeners\LoginViaRemember;
253 |
254 | class EventServiceProvider extends ServiceProvider
255 | {
256 | protected $listen = [
257 | Login::class => [
258 | LoginViaRemember::class,
259 | ],
260 | ];
261 | ...
262 | ```
263 |
264 | ## Events
265 |
266 | The following events are fired:
267 |
268 | - EmptyOneTimePasswordReceived
269 | - LoggedOut
270 | - LoginFailed
271 | - LoginSucceeded
272 | - OneTimePasswordExpired
273 | - OneTimePasswordRequested
274 |
275 | ## Documentation
276 |
277 | Check the ReadMe file in the main [Google2FA](https://github.com/antonioribeiro/google2fa) repository.
278 |
279 | ## Tests
280 |
281 | The package tests were written with [phpspec](http://www.phpspec.net/en/latest/).
282 |
283 | ## Author
284 |
285 | [Antonio Carlos Ribeiro](http://twitter.com/iantonioribeiro)
286 |
287 | ## License
288 |
289 | Google2FA is licensed under the MIT License - see the [LICENSE](LICENSE) file for details
290 |
291 | ## Contributing
292 |
293 | Pull requests and issues are more than welcome.
294 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pragmarx/google2fa-laravel",
3 | "description": "A One Time Password Authentication package, compatible with Google Authenticator.",
4 | "keywords": ["authentication", "two factor authentication", "google2fa", "laravel"],
5 | "license": "MIT",
6 | "authors": [
7 | {
8 | "name": "Antonio Carlos Ribeiro",
9 | "email": "acr@antoniocarlosribeiro.com",
10 | "role": "Creator & Designer"
11 | }
12 | ],
13 | "require": {
14 | "php": ">=7.0",
15 | "laravel/framework": "^5.4.36|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
16 | "pragmarx/google2fa-qrcode": "^1.0|^2.0|^3.0"
17 | },
18 | "require-dev": {
19 | "phpunit/phpunit": "~5|~6|~7|~8|~9|~10|~11",
20 | "orchestra/testbench": "3.4.*|3.5.*|3.6.*|3.7.*|4.*|5.*|6.*|7.*|8.*|9.*|10.*",
21 | "bacon/bacon-qr-code": "^2.0"
22 | },
23 | "autoload": {
24 | "psr-4": {
25 | "PragmaRX\\Google2FALaravel\\": "src/",
26 | "PragmaRX\\Google2FALaravel\\Tests\\": "tests/"
27 | }
28 | },
29 | "extra": {
30 | "component": "package",
31 | "frameworks": ["Laravel"],
32 | "branch-alias": {
33 | "dev-master": "0.2-dev"
34 | },
35 | "laravel": {
36 | "providers": [
37 | "PragmaRX\\Google2FALaravel\\ServiceProvider"
38 | ],
39 | "aliases": {
40 | "Google2FA": "PragmaRX\\Google2FALaravel\\Facade"
41 | }
42 | }
43 | },
44 | "suggest": {
45 | "bacon/bacon-qr-code": "Required to generate inline QR Codes.",
46 | "pragmarx/recovery": "Generate recovery codes."
47 | },
48 | "minimum-stability": "dev",
49 | "prefer-stable": true
50 | }
51 |
--------------------------------------------------------------------------------
/docs/middleware.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antonioribeiro/google2fa-laravel/60f363c16db1e94263e0560efebe9fc2e302b7ef/docs/middleware.jpg
--------------------------------------------------------------------------------
/phpspec.yml:
--------------------------------------------------------------------------------
1 | formatter.name: pretty
2 | suites:
3 | main_suite:
4 | namespace: PragmaRX\Google2FALaravel
5 | src_path: src
6 | spec_path: tests
7 | spec_prefix: spec
8 | psr4_prefix: PragmaRX\Google2FALaravel
9 | extensions:
10 | PhpSpec\Laravel\Extension\LaravelExtension:
11 | testing_environment: "testing"
12 | framework_path: ../../../../bootstrap/app.php
13 |
14 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 | ./tests
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | ./src
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/Events/EmptyOneTimePasswordReceived.php:
--------------------------------------------------------------------------------
1 | user = $user;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Events/LoginFailed.php:
--------------------------------------------------------------------------------
1 | user = $user;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Events/LoginSucceeded.php:
--------------------------------------------------------------------------------
1 | user = $user;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Events/OneTimePasswordExpired.php:
--------------------------------------------------------------------------------
1 | user = $user;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Events/OneTimePasswordRequested.php:
--------------------------------------------------------------------------------
1 | user = $user;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Events/OneTimePasswordRequested53.php:
--------------------------------------------------------------------------------
1 | user = $user;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Exceptions/InvalidOneTimePassword.php:
--------------------------------------------------------------------------------
1 | logout();
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Google2FA.php:
--------------------------------------------------------------------------------
1 | getQRCodeBackend()) {
35 | case Constants::QRCODE_IMAGE_BACKEND_SVG:
36 | return new \BaconQrCode\Renderer\Image\SvgImageBackEnd();
37 |
38 | case Constants::QRCODE_IMAGE_BACKEND_EPS:
39 | return new \BaconQrCode\Renderer\Image\EpsImageBackEnd();
40 |
41 | case Constants::QRCODE_IMAGE_BACKEND_IMAGEMAGICK:
42 | default:
43 | return null;
44 | }
45 | }
46 |
47 | /**
48 | * Set the QRCode Backend.
49 | *
50 | * @param string $qrCodeBackend
51 | *
52 | * @return self
53 | */
54 | public function setQrCodeBackend(string $qrCodeBackend)
55 | {
56 | $this->qrCodeBackend = $qrCodeBackend;
57 |
58 | return $this;
59 | }
60 |
61 | /**
62 | * Authenticator constructor.
63 | *
64 | * @param IlluminateRequest $request
65 | */
66 | public function __construct(IlluminateRequest $request)
67 | {
68 | $this->boot($request);
69 |
70 | parent::__construct(null, $this->getImageBackend());
71 | }
72 |
73 | /**
74 | * Authenticator boot.
75 | *
76 | * @param $request
77 | *
78 | * @return Google2FA
79 | */
80 | public function boot($request)
81 | {
82 | $this->setRequest($request);
83 |
84 | $this->setWindow($this->config('window'));
85 |
86 | return $this;
87 | }
88 |
89 | /**
90 | * The QRCode Backend.
91 | *
92 | * @return mixed
93 | */
94 | public function getQRCodeBackend()
95 | {
96 | return $this->qrCodeBackend
97 | ?: $this->config('qrcode_image_backend', Constants::QRCODE_IMAGE_BACKEND_IMAGEMAGICK);
98 | }
99 |
100 | /**
101 | * Get the user Google2FA secret.
102 | *
103 | * @throws InvalidSecretKey
104 | *
105 | * @return mixed
106 | */
107 | protected function getGoogle2FASecretKey()
108 | {
109 | return $this->getUser()->{$this->config('otp_secret_column')};
110 | }
111 |
112 | /**
113 | * Check if the 2FA is activated for the user.
114 | *
115 | * @return bool
116 | */
117 | public function isActivated()
118 | {
119 | $secret = $this->getGoogle2FASecretKey();
120 |
121 | return !is_null($secret) && !empty($secret);
122 | }
123 |
124 | /**
125 | * Store the old OTP timestamp.
126 | *
127 | * @param $key
128 | *
129 | * @return mixed
130 | */
131 | protected function storeOldTimestamp($key)
132 | {
133 | return $this->config('forbid_old_passwords') === true
134 | ? $this->sessionPut(Constants::SESSION_OTP_TIMESTAMP, $key)
135 | : $key;
136 | }
137 |
138 | /**
139 | * Get the previous OTP timestamp.
140 | *
141 | * @return null|mixed
142 | */
143 | protected function getOldTimestamp()
144 | {
145 | return $this->config('forbid_old_passwords') === true
146 | ? $this->sessionGet(Constants::SESSION_OTP_TIMESTAMP)
147 | : null;
148 | }
149 |
150 | /**
151 | * Keep this OTP session alive.
152 | */
153 | protected function keepAlive()
154 | {
155 | if ($this->config('keep_alive')) {
156 | $this->updateCurrentAuthTime();
157 | }
158 | }
159 |
160 | /**
161 | * Get minutes since last activity.
162 | *
163 | * @return int
164 | */
165 | protected function minutesSinceLastActivity()
166 | {
167 | return Carbon::now()->diffInMinutes(
168 | $this->sessionGet(Constants::SESSION_AUTH_TIME),
169 | true
170 | );
171 | }
172 |
173 | /**
174 | * Check if no user is authenticated using OTP.
175 | *
176 | * @return bool
177 | */
178 | protected function noUserIsAuthenticated()
179 | {
180 | return is_null($this->getUser());
181 | }
182 |
183 | /**
184 | * Check if OTP has expired.
185 | *
186 | * @return bool
187 | */
188 | protected function passwordExpired()
189 | {
190 | if (($minutes = $this->config('lifetime')) !== 0 && $this->minutesSinceLastActivity() > $minutes) {
191 | event(new OneTimePasswordExpired($this->getUser()));
192 |
193 | $this->logout();
194 |
195 | return true;
196 | }
197 |
198 | $this->keepAlive();
199 |
200 | return false;
201 | }
202 |
203 | /**
204 | * Verifies, in the current session, if a 2fa check has already passed.
205 | *
206 | * @return bool
207 | */
208 | protected function twoFactorAuthStillValid()
209 | {
210 | return
211 | (bool) $this->sessionGet(Constants::SESSION_AUTH_PASSED, false) &&
212 | !$this->passwordExpired();
213 | }
214 |
215 | /**
216 | * Check if the module is enabled.
217 | *
218 | * @return mixed
219 | */
220 | protected function isEnabled()
221 | {
222 | return $this->config('enabled');
223 | }
224 |
225 | /**
226 | * Set current auth as valid.
227 | */
228 | public function login()
229 | {
230 | $this->sessionPut(Constants::SESSION_AUTH_PASSED, true);
231 |
232 | $this->updateCurrentAuthTime();
233 | }
234 |
235 | /**
236 | * OTP logout.
237 | */
238 | public function logout()
239 | {
240 | $user = $this->getUser();
241 |
242 | $this->sessionForget();
243 |
244 | event(new LoggedOut($user));
245 | }
246 |
247 | /**
248 | * Update the current auth time.
249 | */
250 | protected function updateCurrentAuthTime()
251 | {
252 | $this->sessionPut(Constants::SESSION_AUTH_TIME, Carbon::now()->toIso8601String());
253 | }
254 |
255 | /**
256 | * Verify the OTP.
257 | *
258 | * @param $secret
259 | * @param $one_time_password
260 | *
261 | * @return mixed
262 | */
263 | public function verifyGoogle2FA($secret, $one_time_password)
264 | {
265 | return $this->verifyKey(
266 | $secret,
267 | $one_time_password,
268 | $this->getWindow(),
269 | null, // $timestamp
270 | $this->getOldTimestamp() ?: null
271 | );
272 | }
273 |
274 | /**
275 | * Verify the OTP and store the timestamp.
276 | *
277 | * @param $one_time_password
278 | *
279 | * @return mixed
280 | */
281 | protected function verifyAndStoreOneTimePassword($one_time_password)
282 | {
283 | return $this->storeOldTimestamp(
284 | $this->verifyGoogle2FA(
285 | $this->getGoogle2FASecretKey(),
286 | $one_time_password
287 | )
288 | );
289 | }
290 | }
291 |
--------------------------------------------------------------------------------
/src/Listeners/LoginViaRemember.php:
--------------------------------------------------------------------------------
1 | registerGoogle2fa($event->user);
23 | }
24 | }
25 |
26 | /**
27 | * Force register Google2fa login.
28 | *
29 | * @param User $user
30 | */
31 | private function registerGoogle2fa(User $user)
32 | {
33 | $secret = $user->{Google2FA::config('otp_secret_column')};
34 |
35 | if (!is_null($secret) && !empty($secret)) {
36 | Google2FA::login();
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Middleware.php:
--------------------------------------------------------------------------------
1 | boot($request);
13 |
14 | if ($authenticator->isAuthenticated()) {
15 | return $next($request);
16 | }
17 |
18 | return $authenticator->makeRequestOneTimePasswordResponse();
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/MiddlewareStateless.php:
--------------------------------------------------------------------------------
1 | bootStateless($request);
13 |
14 | if ($authenticator->isAuthenticated()) {
15 | return $next($request);
16 | }
17 |
18 | return $authenticator->makeRequestOneTimePasswordResponse();
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/ServiceProvider.php:
--------------------------------------------------------------------------------
1 | publishes([
15 | __DIR__.'/config/config.php' => config_path('google2fa.php'),
16 | ]);
17 | }
18 |
19 | /**
20 | * Merge configuration.
21 | */
22 | private function mergeConfig()
23 | {
24 | $this->mergeConfigFrom(
25 | __DIR__.'/config/config.php',
26 | 'google2fa'
27 | );
28 | }
29 |
30 | /**
31 | * Register the service provider.
32 | *
33 | * @return void
34 | */
35 | public function register()
36 | {
37 | $this->app->singleton('pragmarx.google2fa', function ($app) {
38 | return $app->make(Google2FA::class);
39 | });
40 | }
41 |
42 | public function boot()
43 | {
44 | $this->configurePaths();
45 |
46 | $this->mergeConfig();
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Support/Auth.php:
--------------------------------------------------------------------------------
1 | auth)) {
22 | $this->auth = app($this->config('auth'));
23 |
24 | if (!empty($this->config('guard'))) {
25 | $this->auth = app($this->config('auth'))->guard($this->config('guard'));
26 | }
27 | }
28 |
29 | return $this->auth;
30 | }
31 |
32 | /**
33 | * Get the current user.
34 | *
35 | * @return mixed
36 | */
37 | protected function getUser()
38 | {
39 | return $this->getAuth()->user();
40 | }
41 |
42 | abstract protected function config($string, $children = []);
43 | }
44 |
--------------------------------------------------------------------------------
/src/Support/Authenticator.php:
--------------------------------------------------------------------------------
1 | boot($request);
59 |
60 | $this->setStateless();
61 |
62 | return $this;
63 | }
64 |
65 | /**
66 | * Fire login (success or failed).
67 | *
68 | * @param $succeeded
69 | */
70 | private function fireLoginEvent($succeeded)
71 | {
72 | event(
73 | $succeeded
74 | ? new LoginSucceeded($this->getUser())
75 | : new LoginFailed($this->getUser())
76 | );
77 |
78 | return $succeeded;
79 | }
80 |
81 | /**
82 | * Get the OTP from user input.
83 | *
84 | * @throws InvalidOneTimePassword
85 | *
86 | * @return mixed
87 | */
88 | protected function getOneTimePassword()
89 | {
90 | $password = $this->getInputOneTimePassword();
91 |
92 | if (is_null($password) || empty($password)) {
93 | event(new EmptyOneTimePasswordReceived());
94 |
95 | if ($this->config('throw_exceptions', true)) {
96 | throw new InvalidOneTimePassword(config('google2fa.error_messages.cannot_be_empty'));
97 | }
98 | }
99 |
100 | return $password;
101 | }
102 |
103 | /**
104 | * Check if the current use is authenticated via OTP.
105 | *
106 | * @return bool
107 | */
108 | public function isAuthenticated()
109 | {
110 | return $this->canPassWithoutCheckingOTP() || ($this->checkOTP() === Constants::OTP_VALID);
111 | }
112 |
113 | /**
114 | * Check if it is already logged in or passable without checking for an OTP.
115 | *
116 | * @return bool
117 | */
118 | protected function canPassWithoutCheckingOTP()
119 | {
120 | return
121 | !$this->isEnabled() ||
122 | $this->noUserIsAuthenticated() ||
123 | !$this->isActivated() ||
124 | $this->twoFactorAuthStillValid();
125 | }
126 |
127 | /**
128 | * Check if the input OTP is valid. Returns one of the possible OTP_STATUS codes:
129 | * 'empty', 'valid' or 'invalid'.
130 | *
131 | * @return string
132 | */
133 | protected function checkOTP()
134 | {
135 | if (!$this->inputHasOneTimePassword() || empty($this->getInputOneTimePassword())) {
136 | return Constants::OTP_EMPTY;
137 | }
138 |
139 | $isValid = $this->verifyOneTimePassword();
140 |
141 | if ($isValid) {
142 | $this->login();
143 | $this->fireLoginEvent($isValid);
144 |
145 | return Constants::OTP_VALID;
146 | }
147 |
148 | $this->fireLoginEvent($isValid);
149 |
150 | return Constants::OTP_INVALID;
151 | }
152 |
153 | /**
154 | * Verify the OTP.
155 | *
156 | * @throws InvalidOneTimePassword
157 | *
158 | * @return mixed
159 | */
160 | protected function verifyOneTimePassword()
161 | {
162 | return $this->verifyAndStoreOneTimePassword($this->getOneTimePassword());
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/src/Support/Config.php:
--------------------------------------------------------------------------------
1 | $message,
21 | ]);
22 | }
23 |
24 | /**
25 | * Get a message bag with a message for a particular status code.
26 | *
27 | * @param $statusCode
28 | *
29 | * @return MessageBag
30 | */
31 | protected function getErrorBagForStatusCode($statusCode)
32 | {
33 | $errorMap = [
34 | SymfonyResponse::HTTP_UNPROCESSABLE_ENTITY => 'google2fa.error_messages.wrong_otp',
35 | SymfonyResponse::HTTP_BAD_REQUEST => 'google2fa.error_messages.cannot_be_empty',
36 | ];
37 |
38 | return $this->createErrorBagForMessage(
39 | trans(
40 | config(
41 | array_key_exists($statusCode, $errorMap) ? $errorMap[$statusCode] : 'google2fa.error_messages.unknown',
42 | 'google2fa.error_messages.unknown'
43 | )
44 | )
45 | );
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Support/Input.php:
--------------------------------------------------------------------------------
1 | getRequest()->has($this->config('otp_input'));
15 | }
16 |
17 | protected function getInputOneTimePassword()
18 | {
19 | return $this->getRequest()->input($this->config('otp_input'));
20 | }
21 |
22 | abstract public function getRequest();
23 |
24 | abstract protected function config($string, $children = []);
25 | }
26 |
--------------------------------------------------------------------------------
/src/Support/Request.php:
--------------------------------------------------------------------------------
1 | request;
22 | }
23 |
24 | /**
25 | * Set the request property.
26 | *
27 | * @param mixed $request
28 | *
29 | * @return $this
30 | */
31 | public function setRequest($request)
32 | {
33 | $this->request = $request;
34 |
35 | return $this;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Support/Response.php:
--------------------------------------------------------------------------------
1 | getErrorBagForStatusCode($statusCode),
24 | $statusCode
25 | );
26 | }
27 |
28 | /**
29 | * Make the status code, to respond accordingly.
30 | *
31 | * @return int
32 | */
33 | protected function makeStatusCode()
34 | {
35 | if ($this->getRequest()->isMethod('get') || ($this->checkOTP() === Constants::OTP_VALID)) {
36 | return SymfonyResponse::HTTP_OK;
37 | }
38 |
39 | if ($this->checkOTP() === Constants::OTP_EMPTY) {
40 | return SymfonyResponse::HTTP_BAD_REQUEST;
41 | }
42 |
43 | return SymfonyResponse::HTTP_UNPROCESSABLE_ENTITY;
44 | }
45 |
46 | /**
47 | * Make a web response.
48 | *
49 | * @param $statusCode
50 | *
51 | * @return \Illuminate\Http\Response
52 | */
53 | protected function makeHtmlResponse($statusCode)
54 | {
55 | $view = $this->getView();
56 |
57 | if ($statusCode !== SymfonyResponse::HTTP_OK) {
58 | $view->withErrors($this->getErrorBagForStatusCode($statusCode));
59 | }
60 |
61 | return new IlluminateHtmlResponse($view, $statusCode);
62 | }
63 |
64 | /**
65 | * Create a response to request the OTP.
66 | *
67 | * @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse
68 | */
69 | public function makeRequestOneTimePasswordResponse()
70 | {
71 | event(
72 | app()->version() < '5.4'
73 | ? new OneTimePasswordRequested53($this->getUser())
74 | : new OneTimePasswordRequested($this->getUser())
75 | );
76 |
77 | $expectJson = app()->version() < '5.4'
78 | ? $this->getRequest()->wantsJson()
79 | : $this->getRequest()->expectsJson();
80 |
81 | return $expectJson
82 | ? $this->makeJsonResponse($this->makeStatusCode())
83 | : $this->makeHtmlResponse($this->makeStatusCode());
84 | }
85 |
86 | /**
87 | * Get the OTP view.
88 | *
89 | * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
90 | */
91 | private function getView()
92 | {
93 | return view($this->config('view'));
94 | }
95 |
96 | abstract protected function getErrorBagForStatusCode($statusCode);
97 |
98 | abstract protected function inputHasOneTimePassword();
99 |
100 | abstract public function checkOTP();
101 |
102 | abstract protected function getUser();
103 |
104 | abstract public function getRequest();
105 |
106 | abstract protected function config($string, $children = []);
107 | }
108 |
--------------------------------------------------------------------------------
/src/Support/Session.php:
--------------------------------------------------------------------------------
1 | config('session_var').(is_null($name) || empty($name) ? '' : '.'.$name);
24 | }
25 |
26 | /**
27 | * Get a session var value.
28 | *
29 | * @param null $var
30 | *
31 | * @return mixed
32 | */
33 | public function sessionGet($var = null, $default = null)
34 | {
35 | if ($this->stateless) {
36 | return $default;
37 | }
38 |
39 | return $this->getRequest()->session()->get(
40 | $this->makeSessionVarName($var),
41 | $default
42 | );
43 | }
44 |
45 | /**
46 | * Put a var value to the current session.
47 | *
48 | * @param $var
49 | * @param $value
50 | *
51 | * @return mixed
52 | */
53 | protected function sessionPut($var, $value)
54 | {
55 | if ($this->stateless) {
56 | return $value;
57 | }
58 |
59 | $this->getRequest()->session()->put(
60 | $this->makeSessionVarName($var),
61 | $value
62 | );
63 |
64 | return $value;
65 | }
66 |
67 | /**
68 | * Forget a session var.
69 | *
70 | * @param null $var
71 | */
72 | protected function sessionForget($var = null)
73 | {
74 | if ($this->stateless) {
75 | return;
76 | }
77 |
78 | $this->getRequest()->session()->forget(
79 | $this->makeSessionVarName($var)
80 | );
81 | }
82 |
83 | /**
84 | * @param mixed $stateless
85 | *
86 | * @return Authenticator
87 | */
88 | public function setStateless($stateless = true)
89 | {
90 | $this->stateless = $stateless;
91 |
92 | return $this;
93 | }
94 |
95 | abstract protected function config($string, $children = []);
96 |
97 | abstract public function getRequest();
98 | }
99 |
--------------------------------------------------------------------------------
/src/config/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antonioribeiro/google2fa-laravel/60f363c16db1e94263e0560efebe9fc2e302b7ef/src/config/.gitkeep
--------------------------------------------------------------------------------
/src/config/config.php:
--------------------------------------------------------------------------------
1 | env('OTP_ENABLED', true),
9 |
10 | /*
11 | * Lifetime in minutes.
12 | *
13 | * In case you need your users to be asked for a new one time passwords from time to time.
14 | */
15 | 'lifetime' => env('OTP_LIFETIME', 0), // 0 = eternal
16 |
17 | /*
18 | * Renew lifetime at every new request.
19 | */
20 | 'keep_alive' => env('OTP_KEEP_ALIVE', true),
21 |
22 | /*
23 | * Auth container binding.
24 | */
25 | 'auth' => 'auth',
26 |
27 | /*
28 | * Guard.
29 | */
30 | 'guard' => '',
31 |
32 | /*
33 | * 2FA verified session var.
34 | */
35 | 'session_var' => 'google2fa',
36 |
37 | /*
38 | * One Time Password request input name.
39 | */
40 | 'otp_input' => 'one_time_password',
41 |
42 | /*
43 | * One Time Password Window.
44 | */
45 | 'window' => 1,
46 |
47 | /*
48 | * Forbid user to reuse One Time Passwords.
49 | */
50 | 'forbid_old_passwords' => false,
51 |
52 | /*
53 | * User's table column for google2fa secret.
54 | */
55 | 'otp_secret_column' => 'google2fa_secret',
56 |
57 | /*
58 | * One Time Password View.
59 | */
60 | 'view' => 'google2fa.index',
61 |
62 | /*
63 | * One Time Password error message.
64 | */
65 | 'error_messages' => [
66 | 'wrong_otp' => "The 'One Time Password' typed was wrong.",
67 | 'cannot_be_empty' => 'One Time Password cannot be empty.',
68 | 'unknown' => 'An unknown error has occurred. Please try again.',
69 | ],
70 |
71 | /*
72 | * Throw exceptions or just fire events?
73 | */
74 | 'throw_exceptions' => env('OTP_THROW_EXCEPTION', true),
75 |
76 | /*
77 | * Which image backend to use for generating QR codes?
78 | *
79 | * Supports imagemagick, svg and eps
80 | */
81 | 'qrcode_image_backend' => \PragmaRX\Google2FALaravel\Support\Constants::QRCODE_IMAGE_BACKEND_SVG,
82 |
83 | ];
84 |
--------------------------------------------------------------------------------
/tests/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antonioribeiro/google2fa-laravel/60f363c16db1e94263e0560efebe9fc2e302b7ef/tests/.gitkeep
--------------------------------------------------------------------------------
/tests/Constants.php:
--------------------------------------------------------------------------------
1 | createFromBase(
25 | \Symfony\Component\HttpFoundation\Request::create(
26 | '/',
27 | 'GET'
28 | )
29 | );
30 | }
31 |
32 | protected function getPackageProviders($app)
33 | {
34 | return [
35 | \PragmaRX\Google2FALaravel\ServiceProvider::class,
36 | \Illuminate\Auth\AuthServiceProvider::class,
37 | ];
38 | }
39 |
40 | public function setUp(): void
41 | {
42 | parent::setup();
43 |
44 | $this->app->make('Illuminate\Contracts\Http\Kernel')
45 | ->pushMiddleware('Illuminate\Session\Middleware\StartSession')
46 | ->pushMiddleware('Illuminate\View\Middleware\ShareErrorsFromSession::class');
47 |
48 | View::addLocation(__DIR__.'/views');
49 |
50 | $this->loginUser();
51 | }
52 |
53 | protected function getPackageAliases($app)
54 | {
55 | return [
56 | 'Google2FA' => \PragmaRX\Google2FALaravel\Facade::class,
57 | 'Auth' => \Illuminate\Support\Facades\Auth::class,
58 | ];
59 | }
60 |
61 | protected function home()
62 | {
63 | return $this->call('GET', 'home')->getContent();
64 | }
65 |
66 | private function loginUser()
67 | {
68 | $user = new User();
69 |
70 | $user->username = 'foo';
71 |
72 | $user->google2fa_secret = Constants::SECRET;
73 |
74 | Auth::login($user);
75 | }
76 |
77 | protected function assertLogin($password = null, $message = 'google2fa passed')
78 | {
79 | config(['google2fa.error_messages.wrong_otp' => self::WRONG_OTP_ERROR_MESSAGE]);
80 |
81 | $renderedView = $this->call('POST', 'login', ['one_time_password' => $password])->getContent();
82 |
83 | $this->assertStringContainsString(
84 | $message,
85 | $renderedView
86 | );
87 |
88 | if ($message !== self::WRONG_OTP_ERROR_MESSAGE) {
89 | $this->assertStringNotContainsString(
90 | self::WRONG_OTP_ERROR_MESSAGE,
91 | $renderedView
92 | );
93 | }
94 | }
95 |
96 | protected function getEnvironmentSetUp($app)
97 | {
98 | config(['app.debug' => true]);
99 |
100 | $app['router']->get('home', ['as' => 'home', 'uses' => function () {
101 | return 'we are home';
102 | }])->middleware(\PragmaRX\Google2FALaravel\Middleware::class);
103 |
104 | $app['router']->post('login', ['as' => 'login.post', 'uses' => function () {
105 | return 'google2fa passed';
106 | }])->middleware(\PragmaRX\Google2FALaravel\Middleware::class);
107 |
108 | $app['router']->post('logout', ['as' => 'logout.post', 'uses' => function () {
109 | Google2FA::logout();
110 | }]);
111 | }
112 |
113 | public function getOTP()
114 | {
115 | return Google2FA::getCurrentOtp(Auth::user()->google2fa_secret);
116 | }
117 |
118 | // --------------------------------------------- tests
119 |
120 | public function testCanInstantiate()
121 | {
122 | $this->assertEquals(16, strlen(Google2FA::generateSecretKey()));
123 | }
124 |
125 | public function testIsActivated()
126 | {
127 | $this->assertTrue(Google2FA::isActivated());
128 | }
129 |
130 | public function testVerifyGoogle2FA()
131 | {
132 | $this->assertFalse(Google2FA::verifyGoogle2FA(Auth::user()->google2fa_secret, '000000'));
133 | }
134 |
135 | public function testRedirectToGoogle2FAView()
136 | {
137 | $this->assertStringContainsString(
138 | 'google2fa view',
139 | $this->home()
140 | );
141 | }
142 |
143 | public function testGoogle2FAPostPasses()
144 | {
145 | $this->assertLogin($this->getOTP());
146 |
147 | $this->assertStringContainsString(
148 | 'we are home',
149 | $this->home()
150 | );
151 | }
152 |
153 | public function testWrongOTP()
154 | {
155 | $this->assertLogin('9999999', self::WRONG_OTP_ERROR_MESSAGE);
156 | }
157 |
158 | public function testLogout()
159 | {
160 | $this->assertStringContainsString(
161 | 'google2fa view',
162 | $this->home()
163 | );
164 |
165 | $this->assertLogin($this->getOTP());
166 |
167 | $this->assertStringContainsString(
168 | 'we are home',
169 | $this->home()
170 | );
171 |
172 | $this->assertStringContainsString(
173 | '',
174 | $this->call('POST', 'logout')->getContent()
175 | );
176 |
177 | $this->assertStringContainsString(
178 | 'google2fa view',
179 | $this->home()
180 | );
181 | }
182 |
183 | public function testLogin()
184 | {
185 | $this->startSession();
186 |
187 | $request = $this->createEmptyRequest();
188 | $request->setLaravelSession($this->app['session.store']);
189 |
190 | $authenticator = app(\PragmaRX\Google2FALaravel\Google2FA::class)->boot($request);
191 |
192 | $authenticator->login();
193 |
194 | $this->assertTrue($request->getSession()->get('google2fa.auth_passed'));
195 | }
196 |
197 | public function testOldPasswords()
198 | {
199 | config(['google2fa.forbid_old_passwords' => true]);
200 |
201 | $this->assertStringContainsString(
202 | 'google2fa view',
203 | $this->home()
204 | );
205 |
206 | $this->assertLogin($this->getOTP());
207 |
208 | $this->assertStringContainsString(
209 | 'we are home',
210 | $this->home()
211 | );
212 |
213 | $this->assertStringContainsString(
214 | '',
215 | $this->call('POST', 'logout')->getContent()
216 | );
217 |
218 | $this->assertStringContainsString(
219 | 'google2fa view',
220 | $this->home()
221 | );
222 | }
223 |
224 | public function testPasswordExpiration()
225 | {
226 | config(['google2fa.lifetime' => 1]);
227 |
228 | $this->assertLogin($this->getOTP());
229 |
230 | $this->assertStringContainsString(
231 | 'we are home',
232 | $this->home()
233 | );
234 |
235 | Carbon::setTestNow(Carbon::now()->addMinutes(3));
236 |
237 | $this->assertStringContainsString(
238 | 'google2fa view',
239 | $this->home()
240 | );
241 | }
242 |
243 | public function testGoogle2FAEmptyPassword()
244 | {
245 | $this->assertLogin('', $message = config('google2fa.error_messages.cannot_be_empty'));
246 |
247 | $this->assertLogin(null, $message);
248 | }
249 |
250 | public function testQrcodeInline()
251 | {
252 | $qrCode = Google2FA::getQRCodeInline('company name', 'email@company.com', Constants::SECRET);
253 |
254 | $this->assertStringStartsWith(
255 | 'assertTrue(
260 | strlen($qrCode) > 1024
261 | );
262 | }
263 |
264 | public function testStateless()
265 | {
266 | $authenticator = app(Authenticator::class)->bootStateless($this->createEmptyRequest());
267 |
268 | $this->assertFalse($authenticator->isAuthenticated());
269 | }
270 |
271 | public function testViewError()
272 | {
273 | config([
274 | 'google2fa.error_messages.cannot_be_empty' => self::EMPTY_OTP_ERROR_MESSAGE,
275 | ]);
276 |
277 | $this->assertStringContainsString(
278 | self::EMPTY_OTP_ERROR_MESSAGE,
279 | $this->call('POST', 'login', ['input_one_time_password_missing' => 'missing'])->getContent()
280 | );
281 | }
282 |
283 | public function testQrCodeBackend()
284 | {
285 | $this->assertEquals(
286 | PackageConstants::QRCODE_IMAGE_BACKEND_SVG,
287 | Google2FA::getQRCodeBackend()
288 | );
289 |
290 | Google2FA::setQRCodeBackend('imagemagick');
291 |
292 | $this->assertEquals(
293 | PackageConstants::QRCODE_IMAGE_BACKEND_IMAGEMAGICK,
294 | Google2FA::getQRCodeBackend()
295 | );
296 | }
297 | }
298 |
--------------------------------------------------------------------------------
/tests/Support/User.php:
--------------------------------------------------------------------------------
1 | email;
38 | }
39 |
40 | /**
41 | * Route notifications for the Slack channel.
42 | *
43 | * @return string
44 | */
45 | public function routeNotificationForSlack()
46 | {
47 | return config('services.slack.webhook_url');
48 | }
49 |
50 | /**
51 | * Get the connection of the entity.
52 | *
53 | * @return string|null
54 | */
55 | public function getQueueableConnection()
56 | {
57 | // TODO: Implement getQueueableConnection() method.
58 | }
59 |
60 | /**
61 | * Retrieve the model for a bound value.
62 | *
63 | * @param mixed $value
64 | * @param string|null $field
65 | *
66 | * @return \Illuminate\Database\Eloquent\Model|null
67 | */
68 | public function resolveRouteBinding($value, $field = null)
69 | {
70 | // TODO: Implement resolveRouteBinding() method.
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 | all() as $message)
4 | {{ $message }}
5 | @endforeach
6 |
--------------------------------------------------------------------------------
/upgrading.md:
--------------------------------------------------------------------------------
1 | # Google2FA
2 |
3 |
--------------------------------------------------------------------------------