├── .editorconfig ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src ├── Authenticator │ ├── Result.php │ └── TwoFactorFormAuthenticator.php ├── Controller │ └── Component │ │ └── TwoFactorAuthComponent.php └── TwoFactorAuthPlugin.php └── tests ├── Fixture └── UsersFixture.php ├── TestCase ├── Authenticator │ └── TwoFactorFormAuthenticatorTest.php └── Controller │ └── Component │ └── TwoFactorAuthComponentTest.php ├── bootstrap.php └── schema.php /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This file is for unifying the coding style for different editors and IDEs. 2 | ; More information at http://editorconfig.org 3 | 4 | root = false 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 4 9 | charset = "utf-8" 10 | end_of_line = lf 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [*.yml] 15 | indent_style = space 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /composer.lock 2 | /phpunit.xml 3 | /vendor 4 | config/Migrations/schema-dump-default.lock 5 | 6 | build/ 7 | tmp/ 8 | .idea/ 9 | nbproject 10 | .phpunit.cache 11 | .phpunit.result.cache 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | dist: jammy 4 | addons: 5 | apt: 6 | packages: 7 | - "libonig5" 8 | 9 | php: 10 | - 8.1 11 | - 8.2 12 | 13 | sudo: false 14 | 15 | env: 16 | matrix: 17 | - DB=sqlite db_dsn='sqlite:///:memory:' 18 | global: 19 | - DEFAULT=1 20 | 21 | matrix: 22 | fast_finish: true 23 | 24 | include: 25 | - php: 8.2 26 | env: PHPCS=1 DEFAULT=0 27 | 28 | - php: 8.2 29 | env: COVERAGE=1 DEFAULT=0 30 | 31 | before_script: 32 | - composer self-update 33 | - composer install --prefer-source --no-interaction --dev 34 | - if [[ $PHPCS == 1 ]]; then composer require --dev cakephp/cakephp-codesniffer:^5.0 ; fi 35 | 36 | script: 37 | - if [[ $COVERAGE == 1 ]]; then export XDEBUG_MODE=coverage && vendor/bin/phpunit --coverage-clover=coverage.xml ; fi 38 | - if [[ $COVERAGE == 1 ]]; then curl -Os https://uploader.codecov.io/latest/linux/codecov && chmod +x codecov && ./codecov -v -f coverage.xml ; fi 39 | 40 | - if [[ $DEFAULT == 1 ]]; then vendor/bin/phpunit --stderr ; fi 41 | 42 | - if [[ $PHPCS == 1 ]]; then vendor/bin/phpcs -n -p --extensions=php --standard=vendor/cakephp/cakephp-codesniffer/CakePHP --ignore=vendor --ignore=tests/bootstrap.php . ; fi 43 | 44 | notifications: 45 | email: false 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Andrej Griniuk 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://app.travis-ci.com/andrej-griniuk/cakephp-two-factor-auth.svg?branch=master)](https://travis-ci.org/andrej-griniuk/cakephp-two-factor-auth) 2 | [![codecov](https://codecov.io/gh/andrej-griniuk/cakephp-two-factor-auth/branch/master/graph/badge.svg)](https://codecov.io/gh/andrej-griniuk/cakephp-two-factor-auth) 3 | [![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE) 4 | 5 | # TwoFactorAuth plugin for CakePHP 6 | 7 | This plugin provides two factor authentication functionality using [RobThree/TwoFactorAuth](https://github.com/RobThree/TwoFactorAuth) library. 8 | Basically, it works similar way CakePHP `FormAuthenticate` does. After submitting correct username/password, if the user has `secret` field set, he will be asked to enter a one-time code. 9 | **Attention:** it only provides authenticate provider and component and does not take care of users signup, management etc. 10 | 11 | ## Requirements 12 | 13 | - CakePHP 5.0+ (use ***^1.3*** version for CakePHP <3.7, ***^2.0*** version for CakePHP 3.x, ***^3.0*** version for CakePHP 4.x) 14 | 15 | ## Installation 16 | 17 | You can install this plugin into your CakePHP application using [Composer][composer]. 18 | 19 | ```bash 20 | composer require andrej-griniuk/cakephp-two-factor-auth 21 | ``` 22 | 23 | ## Usage 24 | 25 | First of all you need to add `secret` field to your users table (field name can be changed to `TwoFactorAuth.Form` authenticator configuration). 26 | ```sql 27 | ALTER TABLE `users` ADD `secret` VARCHAR(255) NULL; 28 | ``` 29 | 30 | Second, you need to load the plugin in your Application.php 31 | 32 | ```php 33 | $this->addPlugin('TwoFactorAuth'); 34 | ``` 35 | 36 | Alternatively, execute the following line: 37 | 38 | ```bash 39 | bin/cake plugin load TwoFactorAuth 40 | ``` 41 | 42 | You can see the default config values [here](https://github.com/andrej-griniuk/cakephp-two-factor-auth/blob/master/src/Authenticator/TwoFactorFormAuthenticator.php) and find out what do they mean [here](https://github.com/RobThree/TwoFactorAuth#usage). To overwrite them, pass them as `TwoFactorForm` authenticator values. 43 | 44 | Then you need to set up authentication in your Application.php as you would [normally do it](https://book.cakephp.org/authentication/2/en/index.html#getting-started), but using `TwoFactorForm` authenticator instead of `Form`, e.g.: 45 | 46 | ```php 47 | class Application extends BaseApplication implements AuthenticationServiceProviderInterface 48 | { 49 | public function bootstrap(): void 50 | { 51 | // Call parent to load bootstrap from files. 52 | parent::bootstrap(); 53 | 54 | $this->addPlugin('TwoFactorAuth'); 55 | $this->addPlugin('Authentication'); 56 | } 57 | 58 | public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue 59 | { 60 | // Various other middlewares for error handling, routing etc. added here. 61 | 62 | // Create an authentication middleware object 63 | $authentication = new AuthenticationMiddleware($this); 64 | 65 | // Add the middleware to the middleware queue. 66 | // Authentication should be added *after* RoutingMiddleware. 67 | // So that subdirectory information and routes are loaded. 68 | $middlewareQueue->add($authentication); 69 | 70 | return $middlewareQueue; 71 | } 72 | 73 | public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface 74 | { 75 | $service = new AuthenticationService(); 76 | $service->setConfig([ 77 | 'unauthenticatedRedirect' => '/users/login', 78 | 'queryParam' => 'redirect', 79 | ]); 80 | 81 | $fields = [ 82 | 'username' => 'username', 83 | 'password' => 'password' 84 | ]; 85 | 86 | // Load the authenticators, you want session first 87 | $service->loadAuthenticator('Authentication.Session'); 88 | $service->loadAuthenticator('TwoFactorAuth.TwoFactorForm', [ 89 | 'fields' => $fields, 90 | 'loginUrl' => '/users/login' 91 | ]); 92 | 93 | // Load identifiers 94 | $service->loadIdentifier('Authentication.Password', compact('fields')); 95 | 96 | return $service; 97 | } 98 | } 99 | ``` 100 | 101 | Next, in your AppController load the `Authentication` and `TwoFactorAuth` components: 102 | 103 | ```php 104 | // in src/Controller/AppController.php 105 | public function initialize() 106 | { 107 | parent::initialize(); 108 | 109 | $this->loadComponent('Authentication.Authentication'); 110 | $this->loadComponent('TwoFactorAuth.TwoFactorAuth'); 111 | } 112 | ``` 113 | 114 | Once you have the middleware applied to your application you’ll need a way for users to login. A simplistic `UsersController` would look like: 115 | 116 | ```php 117 | class UsersController extends AppController 118 | { 119 | public function beforeFilter(\Cake\Event\EventInterface $event) 120 | { 121 | parent::beforeFilter($event); 122 | 123 | $this->Authentication->allowUnauthenticated(['login', 'verify']); 124 | } 125 | 126 | public function login() 127 | { 128 | $result = $this->Authentication->getResult(); 129 | if ($result->isValid()) { 130 | // If the user is logged in send them away. 131 | $target = $this->Authentication->getLoginRedirect() ?? '/home'; 132 | 133 | return $this->redirect($target); 134 | } 135 | 136 | if ($this->request->is('post') && !$result->isValid()) { 137 | if ($result->getStatus() == \TwoFactorAuth\Authenticator\Result::TWO_FACTOR_AUTH_FAILED) { 138 | // One time code was entered and it's invalid 139 | $this->Flash->error('Invalid 2FA code'); 140 | 141 | return $this->redirect(['action' => 'verify']); 142 | } elseif ($result->getStatus() == \TwoFactorAuth\Authenticator\Result::TWO_FACTOR_AUTH_REQUIRED) { 143 | // One time code is required and wasn't yet entered - redirect to the verify action 144 | return $this->redirect(['action' => 'verify']); 145 | } else { 146 | $this->Flash->error('Invalid username or password'); 147 | } 148 | } 149 | } 150 | 151 | public function logout() 152 | { 153 | $this->Authentication->logout(); 154 | 155 | return $this->redirect(['action' => 'login']); 156 | } 157 | 158 | public function verify() 159 | { 160 | // This action is only needed to render a vew with one time code form 161 | } 162 | } 163 | ``` 164 | 165 | And `verify.php` would look like: 166 | 167 | ```html 168 |
169 | Form->create(null, ['url' => ['action' => 'login']]) ?> 170 |
171 | 172 | Form->control('code') ?> 173 |
174 | Form->button(__('Continue')); ?> 175 | Form->end() ?> 176 |
177 | ``` 178 | 179 | Basically, it works same way CakePHP `Authentication.Form` authenticator does. 180 | After entering correct username/password combination, if the user has `secret` field (can be overwritten via `TwoFactorAuth.TwoFactorForm` configuration) set he will be redirected to the `verify` action where he is asked to enter a one-time code. 181 | There is no logic behind this action, it only renders the form that has to be submitted to the `loginAction` again with `code` field set. 182 | 183 | You can access the [RobThree\Auth\TwoFactorAuth](https://github.com/RobThree/TwoFactorAuth) instance from your controller via `$this->TwoFactorAuth->getTfa()` or call some of the methods directly on `TwoFactorAuth` component. For example, you can generate user's secret and get QR code data URI for it this way: 184 | ```php 185 | $secret = $this->TwoFactorAuth->createSecret(); 186 | $secretDataUri = $this->TwoFactorAuth->getQRCodeImageAsDataUri('CakePHP:user@email.com', $secret); 187 | ``` 188 | Then display it in your view: 189 | ```php 190 | 191 | ``` 192 | See the library page for full documentation: https://github.com/RobThree/TwoFactorAuth 193 | 194 | ## Bugs & Feedback 195 | 196 | https://github.com/andrej-griniuk/cakephp-two-factor-auth/issues 197 | 198 | ## Credits 199 | 200 | https://github.com/RobThree/TwoFactorAuth 201 | 202 | ## License 203 | 204 | Copyright (c) 2020, [Andrej Griniuk][andrej-griniuk] and licensed under [The MIT License][mit]. 205 | 206 | [cakephp]:http://cakephp.org 207 | [composer]:http://getcomposer.org 208 | [mit]:http://www.opensource.org/licenses/mit-license.php 209 | [andrej-griniuk]:https://github.com/andrej-griniuk 210 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "andrej-griniuk/cakephp-two-factor-auth", 3 | "description": "CakePHP auth component and provider fot two-factor authentication", 4 | "type": "cakephp-plugin", 5 | "keywords": ["cakephp", "auth", "two-factor"], 6 | "homepage": "http://github.com/andrej-griniuk/cakephp-two-factor-auth", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Andrej Griniuk", 11 | "email": "andrej.griniuk@gmail.com" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=8.1", 16 | "cakephp/cakephp": "^5.0", 17 | "cakephp/authentication": "^3.0", 18 | "robthree/twofactorauth": "^2.1" 19 | }, 20 | "require-dev": { 21 | "phpunit/phpunit": "^10" 22 | }, 23 | "autoload": { 24 | "psr-4": { 25 | "TwoFactorAuth\\": "src/" 26 | } 27 | }, 28 | "autoload-dev": { 29 | "psr-4": { 30 | "TwoFactorAuth\\Test\\": "tests/", 31 | "Cake\\Test\\": "vendor/cakephp/cakephp/tests/" 32 | } 33 | }, 34 | "config": { 35 | "allow-plugins": { 36 | "dealerdirect/phpcodesniffer-composer-installer": true 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | tests/TestCase/ 12 | 13 | 14 | 15 | 16 | 17 | src/ 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Authenticator/Result.php: -------------------------------------------------------------------------------- 1 | null, 48 | 'userSessionKey' => 'TwoFactorAuth.user', 49 | 'urlChecker' => 'Authentication.Default', 50 | 'fields' => [ 51 | AbstractIdentifier::CREDENTIAL_USERNAME => 'username', 52 | AbstractIdentifier::CREDENTIAL_PASSWORD => 'password', 53 | ], 54 | 'codeField' => 'code', 55 | 'secretProperty' => 'secret', 56 | 'issuer' => null, 57 | 'digits' => 6, 58 | 'period' => 30, 59 | 'algorithm' => Algorithm::Sha1, 60 | 'qrcodeprovider' => null, 61 | 'rngprovider' => null, 62 | 'timeprovider' => null, 63 | ]; 64 | 65 | /** 66 | * Authenticates the identity contained in a request. Will use the `config.userModel`, and `config.fields` 67 | * to find POST data that is used to find a matching record in the `config.userModel`. Will return false if 68 | * there is no post data, either username or password is missing, or if the scope conditions have not been met. 69 | * 70 | * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information. 71 | * @return \Authentication\Authenticator\ResultInterface 72 | */ 73 | public function authenticate(ServerRequestInterface $request): ResultInterface 74 | { 75 | if (!$this->_checkUrl($request)) { 76 | return $this->_buildLoginUrlErrorResult($request); 77 | } 78 | 79 | $code = Hash::get($request->getParsedBody(), $this->getConfig('codeField')); 80 | if (!is_null($code)) { 81 | return $this->authenticateCode($request, (string)$code); 82 | } else { 83 | return $this->authenticateCredentials($request); 84 | } 85 | } 86 | 87 | /** 88 | * 2nd factor authentication 89 | * 90 | * @param \Psr\Http\Message\ServerRequestInterface $request Request object 91 | * @param string $code One-time code 92 | * @return \Authentication\Authenticator\ResultInterface 93 | */ 94 | protected function authenticateCode(ServerRequestInterface $request, string $code): ResultInterface 95 | { 96 | $user = $this->_getSessionUser($request); 97 | if (!$user) { 98 | // User hasn't passed 1st factor auth 99 | return new Result(null, Result::FAILURE_CREDENTIALS_MISSING); 100 | } 101 | 102 | if (!$this->_verifyCode($this->_getUserSecret($user), $code)) { 103 | // 2nd factor auth code is invalid 104 | return new Result(null, Result::TWO_FACTOR_AUTH_FAILED); 105 | } 106 | 107 | $this->_unsetSessionUser($request); 108 | 109 | return new Result($user, Result::SUCCESS); 110 | } 111 | 112 | /** 113 | * 1st factor authentication 114 | * 115 | * @param \Psr\Http\Message\ServerRequestInterface $request Request object 116 | * @return \Authentication\Authenticator\ResultInterface 117 | */ 118 | protected function authenticateCredentials(ServerRequestInterface $request): ResultInterface 119 | { 120 | $result = parent::authenticate($request); 121 | 122 | if ( 123 | !$result->isValid() 124 | || !$this->_getUser2faEnabledStatus($result->getData()) 125 | || !$this->_getUserSecret($result->getData()) 126 | ) { 127 | // The user is invalid or the 2FA secret is not enabled/present 128 | return $result; 129 | } 130 | 131 | $user = $result->getData(); 132 | 133 | // Store user authenticated with 1 factor 134 | $this->_setSessionUser($request, $user); 135 | 136 | return new Result(null, Result::TWO_FACTOR_AUTH_REQUIRED); 137 | } 138 | 139 | /** 140 | * Verify 2FA code 141 | * 142 | * @param string $secret Secret 143 | * @param string $code One-time code 144 | * @return bool 145 | */ 146 | protected function _verifyCode(string $secret, string $code): bool 147 | { 148 | try { 149 | return $this->getTfa()->verifyCode($secret, $code); 150 | } catch (Exception $e) { 151 | return false; 152 | } 153 | } 154 | 155 | /** 156 | * Get pre-authenticated user from the session 157 | * 158 | * @param \Psr\Http\Message\ServerRequestInterface $request Request object 159 | * @return \ArrayAccess|null 160 | */ 161 | protected function _getSessionUser(ServerRequestInterface $request): ?ArrayAccess 162 | { 163 | /** @var \Cake\Http\Session $session */ 164 | $session = $request->getAttribute('session'); 165 | 166 | return $session->read($this->getConfig('userSessionKey')); 167 | } 168 | 169 | /** 170 | * Store pre-authenticated user in the session 171 | * 172 | * @param \Psr\Http\Message\ServerRequestInterface $request Request object 173 | * @param \ArrayAccess $user User 174 | */ 175 | protected function _setSessionUser(ServerRequestInterface $request, ArrayAccess $user): void 176 | { 177 | /** @var \Cake\Http\Session $session */ 178 | $session = $request->getAttribute('session'); 179 | $session->write($this->getConfig('userSessionKey'), $user); 180 | } 181 | 182 | /** 183 | * Clear pre-authenticated user from the session 184 | * 185 | * @param \Psr\Http\Message\ServerRequestInterface $request Request object 186 | */ 187 | protected function _unsetSessionUser(ServerRequestInterface $request): void 188 | { 189 | /** @var \Cake\Http\Session $session */ 190 | $session = $request->getAttribute('session'); 191 | 192 | $session->delete($this->getConfig('userSessionKey')); 193 | } 194 | 195 | /** 196 | * Get user's 2FA secret 197 | * 198 | * @param \ArrayAccess $user User 199 | * @return string|null 200 | */ 201 | protected function _getUserSecret(ArrayAccess $user): ?string 202 | { 203 | return Hash::get($user, $this->getConfig('secretProperty')); 204 | } 205 | 206 | /** 207 | * Check if 2FA is enabled for the given user 208 | * 209 | * @param \ArrayAccess|array $user User 210 | * @return bool 211 | */ 212 | protected function _getUser2faEnabledStatus(array|ArrayAccess $user): bool 213 | { 214 | return (bool)Hash::get($user, $this->getConfig('isEnabled2faProperty', $this->getConfig('secretProperty'))); 215 | } 216 | 217 | /** 218 | * Get RobThree\Auth\TwoFactorAuth object 219 | * 220 | * @return \RobThree\Auth\TwoFactorAuth 221 | * @throws \RobThree\Auth\TwoFactorAuthException 222 | */ 223 | public function getTfa(): TwoFactorAuth 224 | { 225 | if (!$this->_tfa) { 226 | $this->_tfa = new TwoFactorAuth( 227 | $this->getConfig('issuer'), 228 | $this->getConfig('digits'), 229 | $this->getConfig('period'), 230 | $this->getConfig('algorithm'), 231 | $this->getConfig('qrcodeprovider'), 232 | $this->getConfig('rngprovider'), 233 | $this->getConfig('timeprovider') 234 | ); 235 | } 236 | 237 | return $this->_tfa; 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/Controller/Component/TwoFactorAuthComponent.php: -------------------------------------------------------------------------------- 1 | getTfa()->verifyCode($secret, str_replace(' ', '', $code)); 25 | } 26 | 27 | /** 28 | * Create 2FA secret 29 | * 30 | * @param int $bits Number of bits 31 | * @param bool $requireCryptoSecure Require crypto secure 32 | * @return string 33 | * @throws \RobThree\Auth\TwoFactorAuthException 34 | */ 35 | public function createSecret(int $bits = 80, bool $requireCryptoSecure = true): string 36 | { 37 | return $this->getTfa()->createSecret($bits, $requireCryptoSecure); 38 | } 39 | 40 | /** 41 | * Get data-uri of QRCode 42 | * 43 | * @param string $label Label 44 | * @param string $secret Secret 45 | * @param int $size Size 46 | * @return string 47 | * @throws \RobThree\Auth\TwoFactorAuthException 48 | */ 49 | public function getQRCodeImageAsDataUri(string $label, string $secret, int $size = 200): string 50 | { 51 | return $this->getTfa()->getQRCodeImageAsDataUri($label, $secret, $size); 52 | } 53 | 54 | /** 55 | * Get RobThree\Auth\TwoFactorAuth object 56 | * 57 | * @return \RobThree\Auth\TwoFactorAuth 58 | * @throws \RobThree\Auth\TwoFactorAuthException 59 | */ 60 | public function getTfa(): TwoFactorAuth 61 | { 62 | /** @var \Authentication\AuthenticationService $authenticationService */ 63 | $authenticationService = $this->getController()->getRequest()->getAttribute('authentication'); 64 | 65 | /** @var \TwoFactorAuth\Authenticator\TwoFactorFormAuthenticator $twoFactorFormAuthenticator */ 66 | $twoFactorFormAuthenticator = $authenticationService->authenticators()->get('TwoFactorForm'); 67 | 68 | return $twoFactorFormAuthenticator->getTfa(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/TwoFactorAuthPlugin.php: -------------------------------------------------------------------------------- 1 | 'mariano', 'password' => '$2a$10$u05j8FjsvLBNdfhBhc21LOuVMpzpabVXQ9OpC2wO3pSO0q6t7HHMO', 'secret' => null, 'created' => '2007-03-17 01:16:23', 'updated' => '2007-03-17 01:18:31'], 18 | ['username' => 'nate', 'password' => '$2a$10$u05j8FjsvLBNdfhBhc21LOuVMpzpabVXQ9OpC2wO3pSO0q6t7HHMO', 'secret' => 'FDJBDYSSZMLJBOUG', 'created' => '2008-03-17 01:18:23', 'updated' => '2008-03-17 01:20:31'], 19 | ['username' => 'larry', 'password' => '$2a$10$u05j8FjsvLBNdfhBhc21LOuVMpzpabVXQ9OpC2wO3pSO0q6t7HHMO', 'secret' => null, 'created' => '2010-05-10 01:20:23', 'updated' => '2010-05-10 01:22:31'], 20 | ['username' => 'garrett', 'password' => '$2a$10$u05j8FjsvLBNdfhBhc21LOuVMpzpabVXQ9OpC2wO3pSO0q6t7HHMO', 'secret' => null, 'created' => '2012-06-10 01:22:23', 'updated' => '2012-06-12 01:24:31'], 21 | ]; 22 | } 23 | -------------------------------------------------------------------------------- /tests/TestCase/Authenticator/TwoFactorFormAuthenticatorTest.php: -------------------------------------------------------------------------------- 1 | _setupUsersAndPasswords(); 40 | } 41 | 42 | /** 43 | * _setupUsersAndPasswords 44 | * 45 | * @return void 46 | */ 47 | protected function _setupUsersAndPasswords() 48 | { 49 | TableRegistry::getTableLocator()->clear(); 50 | $this->Users = TableRegistry::getTableLocator()->get( 51 | 'Users', 52 | [ 53 | 'className' => 'TestApp\Model\Table\UsersTable', 54 | ] 55 | ); 56 | 57 | $password = password_hash('password', PASSWORD_DEFAULT); 58 | $this->Users->updateAll(['password' => $password], []); 59 | } 60 | 61 | /** 62 | * testAuthenticate 63 | * 64 | * @return void 65 | */ 66 | public function testAuthenticate() 67 | { 68 | $identifiers = new IdentifierCollection( 69 | [ 70 | 'Authentication.Password', 71 | ] 72 | ); 73 | 74 | $request = ServerRequestFactory::fromGlobals( 75 | ['REQUEST_URI' => '/testpath'], 76 | [], 77 | ['username' => 'mariano', 'password' => 'password'] 78 | ); 79 | 80 | $form = new TwoFactorFormAuthenticator($identifiers); 81 | $result = $form->authenticate($request); 82 | 83 | $this->assertInstanceOf(Result::class, $result); 84 | $this->assertEquals(Result::SUCCESS, $result->getStatus()); 85 | } 86 | 87 | /** 88 | * testCredentialsNotPresent 89 | * 90 | * @return void 91 | */ 92 | public function testCredentialsNotPresent() 93 | { 94 | $identifiers = new IdentifierCollection( 95 | [ 96 | 'Authentication.Password', 97 | ] 98 | ); 99 | 100 | $request = ServerRequestFactory::fromGlobals( 101 | ['REQUEST_URI' => '/users/does-not-match'], 102 | [], 103 | [] 104 | ); 105 | 106 | $form = new TwoFactorFormAuthenticator($identifiers); 107 | 108 | $result = $form->authenticate($request); 109 | 110 | $this->assertInstanceOf(Result::class, $result); 111 | $this->assertEquals(Result::FAILURE_CREDENTIALS_MISSING, $result->getStatus()); 112 | $this->assertEquals([0 => 'Login credentials not found'], $result->getErrors()); 113 | } 114 | 115 | /** 116 | * testCredentialsEmpty 117 | * 118 | * @return void 119 | */ 120 | public function testCredentialsEmpty() 121 | { 122 | $identifiers = new IdentifierCollection( 123 | [ 124 | 'Authentication.Password', 125 | ] 126 | ); 127 | 128 | $request = ServerRequestFactory::fromGlobals( 129 | ['REQUEST_URI' => '/users/does-not-match'], 130 | [], 131 | ['username' => '', 'password' => ''] 132 | ); 133 | 134 | $form = new TwoFactorFormAuthenticator($identifiers); 135 | 136 | $result = $form->authenticate($request); 137 | 138 | $this->assertInstanceOf(Result::class, $result); 139 | $this->assertEquals(Result::FAILURE_CREDENTIALS_MISSING, $result->getStatus()); 140 | $this->assertEquals([0 => 'Login credentials not found'], $result->getErrors()); 141 | } 142 | 143 | /** 144 | * testSingleLoginUrlMismatch 145 | * 146 | * @return void 147 | */ 148 | public function testSingleLoginUrlMismatch() 149 | { 150 | $identifiers = new IdentifierCollection( 151 | [ 152 | 'Authentication.Password', 153 | ] 154 | ); 155 | 156 | $request = ServerRequestFactory::fromGlobals( 157 | ['REQUEST_URI' => '/users/does-not-match'], 158 | [], 159 | ['username' => 'mariano', 'password' => 'password'] 160 | ); 161 | 162 | $form = new TwoFactorFormAuthenticator( 163 | $identifiers, 164 | [ 165 | 'loginUrl' => '/users/login', 166 | ] 167 | ); 168 | 169 | $result = $form->authenticate($request); 170 | 171 | $this->assertInstanceOf(Result::class, $result); 172 | $this->assertEquals(Result::FAILURE_OTHER, $result->getStatus()); 173 | $this->assertEquals([0 => 'Login URL `/users/does-not-match` did not match `/users/login`.'], $result->getErrors()); 174 | } 175 | 176 | /** 177 | * testMultipleLoginUrlMismatch 178 | * 179 | * @return void 180 | */ 181 | public function testMultipleLoginUrlMismatch() 182 | { 183 | $identifiers = new IdentifierCollection( 184 | [ 185 | 'Authentication.Password', 186 | ] 187 | ); 188 | 189 | $request = ServerRequestFactory::fromGlobals( 190 | ['REQUEST_URI' => '/users/does-not-match'], 191 | [], 192 | ['username' => 'mariano', 'password' => 'password'] 193 | ); 194 | 195 | $form = new TwoFactorFormAuthenticator( 196 | $identifiers, 197 | [ 198 | 'loginUrl' => [ 199 | '/en/users/login', 200 | '/de/users/login', 201 | ], 202 | ] 203 | ); 204 | 205 | $result = $form->authenticate($request); 206 | 207 | $this->assertInstanceOf(Result::class, $result); 208 | $this->assertEquals(Result::FAILURE_OTHER, $result->getStatus()); 209 | $this->assertEquals([0 => 'Login URL `/users/does-not-match` did not match `/en/users/login` or `/de/users/login`.'], $result->getErrors()); 210 | } 211 | 212 | /** 213 | * testLoginUrlMismatchWithBase 214 | * 215 | * @return void 216 | */ 217 | public function testLoginUrlMismatchWithBase() 218 | { 219 | $identifiers = new IdentifierCollection( 220 | [ 221 | 'Authentication.Password', 222 | ] 223 | ); 224 | 225 | $request = ServerRequestFactory::fromGlobals( 226 | ['REQUEST_URI' => '/users/login'], 227 | [], 228 | ['username' => 'mariano', 'password' => 'password'] 229 | ); 230 | $uri = $request->getUri(); 231 | $uri->base = '/base'; 232 | $request = $request->withUri($uri); 233 | $request = $request->withAttribute('base', $uri->base); 234 | 235 | $form = new TwoFactorFormAuthenticator( 236 | $identifiers, 237 | [ 238 | 'loginUrl' => '/users/login', 239 | ] 240 | ); 241 | 242 | $result = $form->authenticate($request); 243 | 244 | $this->assertInstanceOf(Result::class, $result); 245 | $this->assertEquals(Result::FAILURE_OTHER, $result->getStatus()); 246 | $this->assertEquals([0 => 'Login URL `/base/users/login` did not match `/users/login`.'], $result->getErrors()); 247 | } 248 | 249 | /** 250 | * testSingleLoginUrlSuccess 251 | * 252 | * @return void 253 | */ 254 | public function testSingleLoginUrlSuccess() 255 | { 256 | $identifiers = new IdentifierCollection( 257 | [ 258 | 'Authentication.Password', 259 | ] 260 | ); 261 | 262 | $request = ServerRequestFactory::fromGlobals( 263 | ['REQUEST_URI' => '/Users/login'], 264 | [], 265 | ['username' => 'mariano', 'password' => 'password'] 266 | ); 267 | 268 | $form = new TwoFactorFormAuthenticator( 269 | $identifiers, 270 | [ 271 | 'loginUrl' => '/Users/login', 272 | ] 273 | ); 274 | 275 | $result = $form->authenticate($request); 276 | 277 | $this->assertInstanceOf(Result::class, $result); 278 | $this->assertEquals(Result::SUCCESS, $result->getStatus()); 279 | $this->assertEquals([], $result->getErrors()); 280 | } 281 | 282 | /** 283 | * testMultipleLoginUrlSuccess 284 | * 285 | * @return void 286 | */ 287 | public function testMultipleLoginUrlSuccess() 288 | { 289 | $identifiers = new IdentifierCollection( 290 | [ 291 | 'Authentication.Password', 292 | ] 293 | ); 294 | 295 | $request = ServerRequestFactory::fromGlobals( 296 | ['REQUEST_URI' => '/de/users/login'], 297 | [], 298 | ['username' => 'mariano', 'password' => 'password'] 299 | ); 300 | 301 | $form = new TwoFactorFormAuthenticator( 302 | $identifiers, 303 | [ 304 | 'loginUrl' => [ 305 | '/en/users/login', 306 | '/de/users/login', 307 | ], 308 | ] 309 | ); 310 | 311 | $result = $form->authenticate($request); 312 | 313 | $this->assertInstanceOf(Result::class, $result); 314 | $this->assertEquals(Result::SUCCESS, $result->getStatus()); 315 | $this->assertEquals([], $result->getErrors()); 316 | } 317 | 318 | /** 319 | * testLoginUrlSuccessWithBase 320 | * 321 | * @return void 322 | */ 323 | public function testLoginUrlSuccessWithBase() 324 | { 325 | $identifiers = new IdentifierCollection( 326 | [ 327 | 'Authentication.Password', 328 | ] 329 | ); 330 | 331 | $request = ServerRequestFactory::fromGlobals( 332 | ['REQUEST_URI' => '/users/login'], 333 | [], 334 | ['username' => 'mariano', 'password' => 'password'] 335 | ); 336 | $uri = $request->getUri(); 337 | $uri->base = '/base'; 338 | $request = $request->withUri($uri); 339 | $request = $request->withAttribute('base', $uri->base); 340 | 341 | $form = new TwoFactorFormAuthenticator( 342 | $identifiers, 343 | [ 344 | 'loginUrl' => '/base/users/login', 345 | ] 346 | ); 347 | 348 | $result = $form->authenticate($request); 349 | 350 | $this->assertInstanceOf(Result::class, $result); 351 | $this->assertEquals(Result::SUCCESS, $result->getStatus()); 352 | $this->assertEquals([], $result->getErrors()); 353 | } 354 | 355 | /** 356 | * testRegexLoginUrlSuccess 357 | * 358 | * @return void 359 | */ 360 | public function testRegexLoginUrlSuccess() 361 | { 362 | $identifiers = new IdentifierCollection( 363 | [ 364 | 'Authentication.Password', 365 | ] 366 | ); 367 | 368 | $request = ServerRequestFactory::fromGlobals( 369 | ['REQUEST_URI' => '/de/users/login'], 370 | [], 371 | ['username' => 'mariano', 'password' => 'password'] 372 | ); 373 | 374 | $form = new TwoFactorFormAuthenticator( 375 | $identifiers, 376 | [ 377 | 'loginUrl' => '%^/[a-z]{2}/users/login/?$%', 378 | 'urlChecker' => [ 379 | 'useRegex' => true, 380 | ], 381 | ] 382 | ); 383 | 384 | $result = $form->authenticate($request); 385 | 386 | $this->assertInstanceOf(Result::class, $result); 387 | $this->assertEquals(Result::SUCCESS, $result->getStatus()); 388 | $this->assertEquals([], $result->getErrors()); 389 | } 390 | 391 | /** 392 | * testFullRegexLoginUrlFailure 393 | * 394 | * @return void 395 | */ 396 | public function testFullRegexLoginUrlFailure() 397 | { 398 | $identifiers = new IdentifierCollection( 399 | [ 400 | 'Authentication.Password', 401 | ] 402 | ); 403 | 404 | $request = ServerRequestFactory::fromGlobals( 405 | [ 406 | 'REQUEST_URI' => '/de/users/login', 407 | ], 408 | [], 409 | ['username' => 'mariano', 'password' => 'password'] 410 | ); 411 | 412 | $form = new TwoFactorFormAuthenticator( 413 | $identifiers, 414 | [ 415 | 'loginUrl' => '%auth\.localhost/[a-z]{2}/users/login/?$%', 416 | 'urlChecker' => [ 417 | 'useRegex' => true, 418 | 'checkFullUrl' => true, 419 | ], 420 | ] 421 | ); 422 | 423 | $result = $form->authenticate($request); 424 | 425 | $this->assertInstanceOf(Result::class, $result); 426 | $this->assertEquals(Result::FAILURE_OTHER, $result->getStatus()); 427 | $this->assertEquals([0 => 'Login URL `http://localhost/de/users/login` did not match `%auth\.localhost/[a-z]{2}/users/login/?$%`.'], $result->getErrors()); 428 | } 429 | 430 | /** 431 | * testRegexLoginUrlSuccess 432 | * 433 | * @return void 434 | */ 435 | public function testFullRegexLoginUrlSuccess() 436 | { 437 | $identifiers = new IdentifierCollection( 438 | [ 439 | 'Authentication.Password', 440 | ] 441 | ); 442 | 443 | $request = ServerRequestFactory::fromGlobals( 444 | [ 445 | 'REQUEST_URI' => '/de/users/login', 446 | 'SERVER_NAME' => 'auth.localhost', 447 | ], 448 | [], 449 | ['username' => 'mariano', 'password' => 'password'] 450 | ); 451 | $response = new Response(); 452 | 453 | $form = new TwoFactorFormAuthenticator( 454 | $identifiers, 455 | [ 456 | 'loginUrl' => '%auth\.localhost/[a-z]{2}/users/login/?$%', 457 | 'urlChecker' => [ 458 | 'useRegex' => true, 459 | 'checkFullUrl' => true, 460 | ], 461 | ] 462 | ); 463 | 464 | $result = $form->authenticate($request, $response); 465 | 466 | $this->assertInstanceOf(Result::class, $result); 467 | $this->assertEquals(Result::SUCCESS, $result->getStatus()); 468 | $this->assertEquals([], $result->getErrors()); 469 | } 470 | 471 | /** 472 | * testAuthenticateCustomFields 473 | * 474 | * @return void 475 | */ 476 | public function testAuthenticateCustomFields() 477 | { 478 | $identifiers = $this->createMock(IdentifierCollection::class); 479 | 480 | $request = ServerRequestFactory::fromGlobals( 481 | ['REQUEST_URI' => '/users/login'], 482 | [], 483 | ['email' => 'mariano@cakephp.org', 'secret' => 'password'] 484 | ); 485 | 486 | $form = new TwoFactorFormAuthenticator( 487 | $identifiers, 488 | [ 489 | 'loginUrl' => '/users/login', 490 | 'fields' => [ 491 | 'username' => 'email', 492 | 'password' => 'secret', 493 | ], 494 | ] 495 | ); 496 | 497 | $identifiers->expects($this->once()) 498 | ->method('identify') 499 | ->with( 500 | [ 501 | 'username' => 'mariano@cakephp.org', 502 | 'password' => 'password', 503 | ] 504 | ) 505 | ->willReturn( 506 | [ 507 | 'username' => 'mariano@cakephp.org', 508 | 'password' => 'password', 509 | ] 510 | ); 511 | 512 | $form->authenticate($request); 513 | } 514 | 515 | /** 516 | * testAuthenticateValidData 517 | * 518 | * @return void 519 | */ 520 | public function testAuthenticateValidData() 521 | { 522 | $identifiers = $this->createMock(IdentifierCollection::class); 523 | 524 | $request = ServerRequestFactory::fromGlobals( 525 | ['REQUEST_URI' => '/users/login'], 526 | [], 527 | ['id' => 1, 'username' => 'mariano', 'password' => 'password'] 528 | ); 529 | 530 | $form = new TwoFactorFormAuthenticator( 531 | $identifiers, 532 | [ 533 | 'loginUrl' => '/users/login', 534 | ] 535 | ); 536 | 537 | $identifiers->expects($this->once()) 538 | ->method('identify') 539 | ->with( 540 | [ 541 | 'username' => 'mariano', 542 | 'password' => 'password', 543 | ] 544 | ) 545 | ->willReturn( 546 | [ 547 | 'username' => 'mariano', 548 | 'password' => 'password', 549 | ] 550 | ); 551 | 552 | $form->authenticate($request); 553 | } 554 | 555 | /** 556 | * testAuthenticateValidData 557 | * 558 | * @return void 559 | */ 560 | public function testAuthenticateMissingChecker() 561 | { 562 | $identifiers = $this->createMock(IdentifierCollection::class); 563 | 564 | $request = ServerRequestFactory::fromGlobals( 565 | ['REQUEST_URI' => '/users/login'], 566 | [], 567 | ['id' => 1, 'username' => 'mariano', 'password' => 'password'] 568 | ); 569 | 570 | $form = new TwoFactorFormAuthenticator( 571 | $identifiers, 572 | [ 573 | 'loginUrl' => '/users/login', 574 | 'urlChecker' => 'Foo', 575 | ] 576 | ); 577 | 578 | $this->expectException(RuntimeException::class); 579 | $this->expectExceptionMessage('URL checker class `Foo` was not found.'); 580 | 581 | $form->authenticate($request); 582 | } 583 | 584 | /** 585 | * testAuthenticateValidData 586 | * 587 | * @return void 588 | */ 589 | public function testAuthenticateInvalidChecker() 590 | { 591 | $identifiers = $this->createMock(IdentifierCollection::class); 592 | 593 | $request = ServerRequestFactory::fromGlobals( 594 | ['REQUEST_URI' => '/users/login'], 595 | [], 596 | ['id' => 1, 'username' => 'mariano', 'password' => 'password'] 597 | ); 598 | 599 | $form = new TwoFactorFormAuthenticator( 600 | $identifiers, 601 | [ 602 | 'loginUrl' => '/users/login', 603 | 'urlChecker' => self::class, 604 | ] 605 | ); 606 | 607 | $this->expectException(RuntimeException::class); 608 | $this->expectExceptionMessage( 609 | 'The provided URL checker class `TwoFactorAuth\Test\TestCase\Authenticator\TwoFactorFormAuthenticatorTest` ' . 610 | 'does not implement the `Authentication\UrlChecker\UrlCheckerInterface` interface.' 611 | ); 612 | 613 | $form->authenticate($request); 614 | } 615 | 616 | /** 617 | * testAuthenticateTwoFactorRequired 618 | * 619 | * @return void 620 | */ 621 | public function testAuthenticateTwoFactorRequired() 622 | { 623 | $identifiers = new IdentifierCollection( 624 | [ 625 | 'Authentication.Password', 626 | ] 627 | ); 628 | 629 | $request = ServerRequestFactory::fromGlobals( 630 | ['REQUEST_URI' => '/testpath'], 631 | [], 632 | ['username' => 'nate', 'password' => 'password'] 633 | ); 634 | 635 | $form = new TwoFactorFormAuthenticator($identifiers); 636 | $result = $form->authenticate($request); 637 | 638 | $this->assertInstanceOf(Result::class, $result); 639 | $this->assertEquals(Result2::TWO_FACTOR_AUTH_REQUIRED, $result->getStatus()); 640 | } 641 | 642 | /** 643 | * testAuthenticateTwoFactorInvalidCode 644 | * 645 | * @return void 646 | */ 647 | public function testAuthenticateTwoFactorInvalidCode() 648 | { 649 | $identifiers = new IdentifierCollection( 650 | [ 651 | 'Authentication.Password', 652 | ] 653 | ); 654 | 655 | $request = ServerRequestFactory::fromGlobals( 656 | ['REQUEST_URI' => '/testpath'], 657 | [], 658 | ['code' => 123] 659 | ); 660 | $user = $this->Users->find()->where(['username' => 'nate'])->firstOrFail(); 661 | $request->getAttribute('session')->write('TwoFactorAuth.user', $user); 662 | 663 | $form = new TwoFactorFormAuthenticator($identifiers); 664 | $result = $form->authenticate($request); 665 | 666 | $this->assertInstanceOf(Result::class, $result); 667 | $this->assertEquals(Result2::TWO_FACTOR_AUTH_FAILED, $result->getStatus()); 668 | } 669 | 670 | /** 671 | * testAuthenticateTwoFactorWithCodeNoUser 672 | * 673 | * @return void 674 | */ 675 | public function testAuthenticateTwoFactorWithCodeNoUser() 676 | { 677 | $identifiers = new IdentifierCollection( 678 | [ 679 | 'Authentication.Password', 680 | ] 681 | ); 682 | 683 | $request = ServerRequestFactory::fromGlobals( 684 | ['REQUEST_URI' => '/testpath'], 685 | [], 686 | ['code' => 123456] 687 | ); 688 | 689 | $form = new TwoFactorFormAuthenticator($identifiers); 690 | $result = $form->authenticate($request); 691 | 692 | $this->assertInstanceOf(Result::class, $result); 693 | $this->assertEquals(Result::FAILURE_CREDENTIALS_MISSING, $result->getStatus()); 694 | } 695 | 696 | /** 697 | * testAuthenticateTwoFactorCorrectCode 698 | * 699 | * @return void 700 | * @throws TwoFactorAuthException 701 | */ 702 | public function testAuthenticateTwoFactorCorrectCode() 703 | { 704 | $identifiers = new IdentifierCollection( 705 | [ 706 | 'Authentication.Password', 707 | ] 708 | ); 709 | 710 | $user = $this->Users->find()->where(['username' => 'nate'])->firstOrFail(); 711 | $form = new TwoFactorFormAuthenticator($identifiers); 712 | 713 | $request = ServerRequestFactory::fromGlobals( 714 | ['REQUEST_URI' => '/testpath'], 715 | [], 716 | ['code' => $form->getTfa()->getCode($user->secret)] 717 | ); 718 | $request->getAttribute('session')->write('TwoFactorAuth.user', $user); 719 | 720 | $result = $form->authenticate($request); 721 | 722 | $this->assertInstanceOf(Result::class, $result); 723 | $this->assertEquals(Result::SUCCESS, $result->getStatus()); 724 | } 725 | } 726 | -------------------------------------------------------------------------------- /tests/TestCase/Controller/Component/TwoFactorAuthComponentTest.php: -------------------------------------------------------------------------------- 1 | [ 38 | 'Authentication.Password', 39 | ], 40 | 'authenticators' => [ 41 | 'Authentication.Session', 42 | 'TwoFactorAuth.TwoFactorForm', 43 | ], 44 | ] 45 | ); 46 | 47 | $request = ServerRequestFactory::fromGlobals( 48 | ['REQUEST_URI' => '/'], 49 | [], 50 | ['username' => 'mariano', 'password' => 'password'] 51 | ); 52 | $request = $request->withAttribute('authentication', $service); 53 | $controller = new Controller($request, 'Users'); 54 | $registry = new ComponentRegistry($controller); 55 | 56 | $this->TwoFactorAuth = new TwoFactorAuthComponent($registry); 57 | } 58 | 59 | /** 60 | * getTfa method test 61 | * 62 | * @return void 63 | * @throws \RobThree\Auth\TwoFactorAuthException 64 | */ 65 | public function testGetTfa(): void 66 | { 67 | $tfa = $this->TwoFactorAuth->getTfa(); 68 | $this->assertInstanceOf(TwoFactorAuth::class, $tfa); 69 | } 70 | 71 | /** 72 | * createSecret method test 73 | * 74 | * @return void 75 | * @throws \RobThree\Auth\TwoFactorAuthException 76 | */ 77 | public function testCreateSecret(): void 78 | { 79 | $secret = $this->TwoFactorAuth->createSecret(); 80 | $this->assertNotNull($secret); 81 | } 82 | 83 | /** 84 | * verifyCode method test 85 | * 86 | * @return void 87 | * @throws \RobThree\Auth\TwoFactorAuthException 88 | */ 89 | public function testVerifyCodeFailure(): void 90 | { 91 | $secret = $this->TwoFactorAuth->createSecret(); 92 | $this->assertFalse($this->TwoFactorAuth->verifyCode($secret, '123456')); 93 | } 94 | 95 | /** 96 | * verifyCode method test 97 | * 98 | * @return void 99 | * @throws \RobThree\Auth\TwoFactorAuthException 100 | */ 101 | public function testVerifyCodeSuccess(): void 102 | { 103 | $secret = $this->TwoFactorAuth->createSecret(); 104 | $code = $this->TwoFactorAuth->getTfa()->getCode($secret); 105 | $this->assertTrue($this->TwoFactorAuth->verifyCode($secret, $code)); 106 | } 107 | 108 | /** 109 | * getQRCodeImageAsDataUri method test 110 | * 111 | * @return void 112 | * @throws \RobThree\Auth\TwoFactorAuthException 113 | */ 114 | public function testGetQRCodeImageAsDataUri(): void 115 | { 116 | $uri = $this->TwoFactorAuth->getQRCodeImageAsDataUri('label', $this->TwoFactorAuth->getTfa()->createSecret()); 117 | $this->assertNotNull($uri); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 'TwoFactorAuth', 52 | 'paths' => [ 53 | 'plugins' => [ROOT . 'Plugin' . DS], 54 | 'templates' => [ROOT . 'templates' . DS], 55 | ], 56 | 'encoding' => 'UTF-8', 57 | ] 58 | ); 59 | 60 | if (!getenv('db_dsn')) { 61 | putenv('db_dsn=sqlite:///:memory:'); 62 | } 63 | 64 | Plugin::getCollection()->add(new AuthPlugin()); 65 | 66 | $_SERVER['PHP_SELF'] = '/'; 67 | 68 | $loader = new SchemaLoader(); 69 | $loader->loadInternalFile(__DIR__ . '/schema.php'); 70 | -------------------------------------------------------------------------------- /tests/schema.php: -------------------------------------------------------------------------------- 1 | [ 6 | 'columns' => [ 7 | 'id' => ['type' => 'integer'], 8 | 'username' => ['type' => 'string', 'null' => true], 9 | 'password' => ['type' => 'string', 'null' => true], 10 | 'secret' => ['type' => 'string', 'null' => true], 11 | 'created' => ['type' => 'timestamp', 'null' => true], 12 | 'updated' => ['type' => 'timestamp', 'null' => true], 13 | ], 14 | 'constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]], 15 | ], 16 | ]; 17 | --------------------------------------------------------------------------------