├── .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 | [](https://travis-ci.org/andrej-griniuk/cakephp-two-factor-auth)
2 | [](https://codecov.io/gh/andrej-griniuk/cakephp-two-factor-auth)
3 | [](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 | = $this->Form->create(null, ['url' => ['action' => 'login']]) ?>
170 |
174 | = $this->Form->button(__('Continue')); ?>
175 | = $this->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 |
--------------------------------------------------------------------------------