├── .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 | Latest Stable Version 5 | License 6 | Code Quality 7 | Build 8 |

9 |

10 | Downloads 11 | Coverage 12 | StyleCI 13 | PHP 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 | ![playground](https://github.com/antonioribeiro/google2fa/raw/master/docs/playground.jpg) 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 | ![middleware](docs/middleware.jpg) 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 |
171 | 172 | 173 | 174 |
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 | --------------------------------------------------------------------------------