diff --git a/.github/codecov.yml b/.github/codecov.yml new file mode 100644 index 000000000..87fe2a21f --- /dev/null +++ b/.github/codecov.yml @@ -0,0 +1,7 @@ +codecov: + require_ci_to_pass: yes + +coverage: + range: "90...100" + +comment: false \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..032c79a25 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,120 @@ +name: CI + +on: + push: + + pull_request: + branches: + - '*' + +jobs: + testsuite: + runs-on: ubuntu-20.04 + strategy: + fail-fast: false + matrix: + php-version: ['7.3', '7.4', '8.0', '8.1'] + db-type: [sqlite, mysql, pgsql] + prefer-lowest: [''] + + steps: + - name: Setup MySQL latest + if: matrix.db-type == 'mysql' + run: docker run --rm --name=mysqld -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=cakephp -p 3306:3306 -d mysql --default-authentication-plugin=mysql_native_password --disable-log-bin + + - name: Setup PostgreSQL latest + if: matrix.db-type == 'pgsql' + run: docker run --rm --name=postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=cakephp -p 5432:5432 -d postgres + + - uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + extensions: mbstring, intl, apcu, sqlite, pdo_sqlite, pdo_${{ matrix.db-type }}, ${{ matrix.db-type }} + ini-values: apc.enable_cli = 1 + coverage: pcov + + - name: Get composer cache directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Get date part for cache key + id: key-date + run: echo "::set-output name=date::$(date +'%Y-%m')" + + - name: Cache composer dependencies + uses: actions/cache@v1 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ steps.key-date.outputs.date }}-${{ hashFiles('composer.json') }}-${{ matrix.prefer-lowest }} + + - name: composer install + run: | + if ${{ matrix.prefer-lowest == 'prefer-lowest' }}; then + composer update --prefer-lowest --prefer-stable + else + composer update + fi + + - name: Setup problem matchers for PHPUnit + if: matrix.php-version == '7.4' && matrix.db-type == 'mysql' + run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + + - name: Run PHPUnit + run: | + if [[ ${{ matrix.db-type }} == 'sqlite' ]]; then export DB_URL='sqlite:///:memory:'; fi + if [[ ${{ matrix.db-type }} == 'mysql' ]]; then export DB_URL='mysql://root:root@127.0.0.1/cakephp?encoding=utf8'; fi + if [[ ${{ matrix.db-type }} == 'pgsql' ]]; then export DB_URL='postgres://postgres:postgres@127.0.0.1/postgres'; fi + if [[ ${{ matrix.php-version }} == '7.4' ]]; then + export CODECOVERAGE=1 && vendor/bin/phpunit --verbose --coverage-clover=coverage.xml + else + vendor/bin/phpunit + fi + + - name: Submit code coverage + if: matrix.php-version == '7.4' + uses: codecov/codecov-action@v1 + + cs-stan: + name: Coding Standard & Static Analysis + runs-on: ubuntu-20.04 + + steps: + - uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.3' + extensions: mbstring, intl, apcu + coverage: none + + - name: Get composer cache directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Get date part for cache key + id: key-date + run: echo "::set-output name=date::$(date +'%Y-%m')" + + - name: Cache composer dependencies + uses: actions/cache@v1 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ steps.key-date.outputs.date }}-${{ hashFiles('composer.json') }}-${{ matrix.prefer-lowest }} + + - name: composer install + run: composer stan-setup + + - name: Run PHP CodeSniffer + run: composer cs-check + +# - name: Run psalm +# if: success() || failure() +# run: vendor/bin/psalm.phar --output-format=github + + - name: Run phpstan + if: success() || failure() + run: composer stan \ No newline at end of file diff --git a/.gitignore b/.gitignore index c89d5a392..f696b2eed 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ composer.lock .php_cs* /coverage +.phpunit.result.cache diff --git a/.semver b/.semver index 215c91863..726c4d95e 100644 --- a/.semver +++ b/.semver @@ -1,5 +1,5 @@ --- -:major: 9 +:major: 11 :minor: 0 -:patch: 2 +:patch: 0 :special: '' diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 10cad1063..000000000 --- a/.travis.yml +++ /dev/null @@ -1,59 +0,0 @@ -language: php - -dist: xenial - -php: - - 7.2 - - 7.3 - - '7.4snapshot' - -sudo: false - -services: - - postgresql - - mysql - -cache: - directories: - - vendor - - $HOME/.composer/cache - -env: - matrix: - - DB=mysql db_dsn='mysql://root@127.0.0.1/cakephp_test?init[]=SET sql_mode = "STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION"' - - DB=pgsql db_dsn='postgres://postgres@127.0.0.1/cakephp_test' - - DB=sqlite db_dsn='sqlite:///:memory:' - - global: - - DEFAULT=1 - -matrix: - fast_finish: true - - include: - - php: 7.3 - env: PHPCS=1 DEFAULT=0 - - - php: 7.3 - env: PHPSTAN=1 DEFAULT=0 - - - php: 7.3 - env: COVERAGE=1 DEFAULT=0 DB=mysql db_dsn='mysql://root@127.0.0.1/cakephp_test?init[]=SET sql_mode = "STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION"' - -before_script: - - composer install --prefer-dist --no-interaction - - if [[ $DB == 'mysql' ]]; then mysql -u root -e 'CREATE DATABASE cakephp_test;'; fi - - if [[ $DB == 'pgsql' ]]; then psql -c 'CREATE DATABASE cakephp_test;' -U postgres; fi - - if [[ $PHPSTAN = 1 ]]; then composer stan-setup; fi - -script: - - if [[ $DEFAULT = 1 ]]; then composer test; fi - - if [[ $COVERAGE = 1 ]]; then composer coverage-test; fi - - if [[ $PHPCS = 1 ]]; then composer cs-check; fi - - if [[ $PHPSTAN = 1 ]]; then composer stan; fi - -after_success: - - if [[ $COVERAGE = 1 ]]; then bash <(curl -s https://codecov.io/bash); fi - -notifications: - email: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 03d173221..78c464692 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,33 @@ Changelog ========= +Releases for CakePHP 4.3 + +* 11.0.0 + * Switched tests to new cakephp schema + * Update to PHPUnit 9.5 + + Releases for CakePHP 4 ------------- +* 9.2.0 + * Switch to github actions + * New event AfterEmailTokenValidation + * Remove deprecations + +* 9.0.5 + * Added change api token shell command + +* 9.0.4 + * Fixed deprecations and stan issues + * Improved docs + * Fixed issue where RememberMe cookie + * Fixed deprecated UserHelper::isAuthorized + +* 9.0.3 + * Ukrainian (uk) by @yarkm13 + * Docs improvements + * Fix DebugKit permissions issues + * 9.0.2 * Added a custom Unauthorized Handler * If logged user access unauthorized url he is redirected to referer url or '/' if no referer url diff --git a/Docs/Documentation/AuthLinkHelper.md b/Docs/Documentation/AuthLinkHelper.md new file mode 100644 index 000000000..ace2bdeb5 --- /dev/null +++ b/Docs/Documentation/AuthLinkHelper.md @@ -0,0 +1,55 @@ +AuthLinkHelper +============= + +The AuthLink Helper has some methods that may be needed if you want to improve your templates and add features to your app in an easy way. +This helper provide two methods that allow you to hide or display links and postLinks based on the permissions file. +No more permissions check in your views ! If the permissions file is update, you do not have to replicate the permissions logic in the views. + +Setup +--------------- + +Enable the Helper in `src/view/AppView.php`: +```php +class AppView extends View +{ + public function initialize() + { + parent::initialize(); + $this->loadHelper('CakeDC/Users.AuthLink'); + } +} +``` + +Link +----------------- + +You can use this helper like the initial [cakePhp HtmlHelper link method](https://book.cakephp.org/4/en/views/helpers/html.html#creating-links) : + +In templates +```diff +- echo $this->Html->link(__d('cake_d_c/users', 'List Users'), ['action' => 'index']) ++ echo $this->AuthLink->link(__d('cake_d_c/users', 'List Users'), ['action' => 'index']) +``` + +PostLink +----------------- + +You can use this helper like the initial [cakePhp FormHelper postLink method](https://book.cakephp.org/4/en/views/helpers/form.html#creating-post-links) : + +In templates +```diff +- echo $this->Form->postLink(__d('cake_d_c/users', 'Delete'), ['action' => 'delete', $user->id], ['confirm' => __d('cake_d_c/users', 'Are you sure you want to delete # {0}?', $user->id)]) ++ echo $this->AuthLink->postLink(__d('cake_d_c/users', 'Delete'), ['action' => 'delete', $user->id], ['confirm' => __d('cake_d_c/users', 'Are you sure you want to delete # {0}?', $user->id)]) +``` + +Before and After +----------------- + +The link method allow you to add two additional parameters in the options array. +Those two parameters are `before` and `after` to quickly inject some html code in the link, like icons etc + +```php +echo $this->AuthLink->link(__d('cake_d_c/users', 'List Users'), ['action' => 'index', 'before' => '']); +``` + +Before and After are only implemented for the link method. diff --git a/Docs/Documentation/Authentication.md b/Docs/Documentation/Authentication.md index 304d8e4dc..9d692dd21 100644 --- a/Docs/Documentation/Authentication.md +++ b/Docs/Documentation/Authentication.md @@ -2,15 +2,15 @@ Authentication ============== This plugin uses the new authentication plugin [cakephp/authentication](https://github.com/cakephp/authentication/) instead of CakePHP Authentication component, but don't worry, the default configuration should be enough for your -projects. +projects. We've tried to simplify configuration as much as possible using defaults, but keep the ability to override them when needed. Authentication Component ------------------------ -The default behavior is to load the authentication component at UsersController, -defining the default urls for loginAction, loginRedirect, logoutRedirect but not requiring +The default behavior is to load the authentication component at UsersController, +defining the default urls for loginAction, loginRedirect, logoutRedirect but not requiring the request to have a identity. If you prefer to load the component yourself you can set 'Auth.AuthenticationComponent.load': @@ -29,7 +29,7 @@ $user = $this->Authentication->getIdentity()->getOriginalData(); ``` The default configuration for Auth.AuthenticationComponent is: -``` +```php [ 'load' => true, 'loginRedirect' => '/', @@ -57,25 +57,23 @@ list of authenticators includes: These authenticators should be enough for your application, but you easily customize it setting the Auth.Authenticators config key. - -For example if you add JWT authenticator you can set: -``` -$authenticators = Configure::read('Auth.Authenticators'); -$authenticators['Jwt'] = [ - 'className' => 'Authentication.Jwt', +For example if you add JWT authenticator you must add this to your config/users.php file: + +```php +'Auth.Authenticators.Jwt' => [ 'queryParam' => 'token', 'skipTwoFactorVerify' => true, -]; -Configure::write('Auth.Authenticators', $authenticators); + 'className' => 'Authentication.Jwt', +], +``` -``` **You may have noticed the 'skipTwoFactorVerify' option, this option is used to identify if a authenticator should skip the two factor flow** The authenticators are loaded by \CakeDC\Users\Loader\AuthenticationServiceLoader class at load authentication service method from plugin object. - + See the full Auth.Authenticators at config/users.php Identifiers @@ -86,11 +84,12 @@ The identifies are defined to work correctly with the default authenticators, we - CakeDC/Users.Social, for Social and SocialPendingEmail authenticators - Authentication.Token, for TokenAuthenticator -As you add more authenticators you may need to add identifiers, please check identifiers available at +As you add more authenticators you may need to add identifiers, please check identifiers available at [official documentation](https://github.com/cakephp/authentication/blob/master/docs/Identifiers.md) The default value for Auth.Identifiers is: -``` + +```php [ 'Password' => [ 'className' => 'Authentication.Password', @@ -127,14 +126,15 @@ For both form login and social login we use a base component 'CakeDC/Users.Login it check the result of authentication service to redirect user to a internal page or show an authentication error. It provide some error messages for specific authentication result status, please check the config/users.php file. -To use a custom component to handle the login you could do: +To use a custom component to handle the login you should update your config/users.php file with: + +```php +'Auth.SocialLoginFailure.component' => 'MyLoginA', +'Auth.FormLoginFailure.component' => 'MyLoginB', ``` -Configure::write('Auth.SocialLoginFailure.component', 'MyLoginA'); -Configure::write('Auth.FormLoginFailure.component', 'MyLoginB'); -``` The default configuration are: -``` +```php [ ... 'Auth' => [ @@ -165,24 +165,24 @@ The default configuration are: ... ] ] -``` +``` -Authentication Service Loader +Authentication Service Loader ----------------------------- To make the integration with cakephp/authentication easier we load the authenticators and identifiers defined at Auth configuration and other components to work with social provider, two-factor authentication. -If the configuration is not enough for your project you may create a custom loader extending the +If the configuration is not enough for your project you may create a custom loader extending the default provided. - Create file src/Loader/AppAuthenticationServiceLoader.php -``` +```php \App\Loader\AppAuthenticationServiceLoader::class, ``` diff --git a/Docs/Documentation/Authorization.md b/Docs/Documentation/Authorization.md index 6575d2d56..58809f524 100644 --- a/Docs/Documentation/Authorization.md +++ b/Docs/Documentation/Authorization.md @@ -6,22 +6,25 @@ projects. We tried to allow you to start quickly without the need to configure a allow you to configure as much as possible. -If you don't want the plugin to autoload setup authorization, you can do: -``` -Configure::write('Auth.Authorization.enabled', false); +If you don't want the plugin to autoload setup authorization, you can disable +in your config/users.php with: + +```php +'Auth.Authorization.enabled' => false, ``` Authorization Middleware ------------------------ We load the RequestAuthorization and Authorization middleware with OrmResolver and RbacProvider(work with RequestAuthorizationMiddleware). -The middleware accepts some additional configurations, you can do: -``` -Configure::write('Auth.AuthorizationMiddleware', $config); +The middleware accepts some additional configurations, you can update in your +config/users.php file: +```php +'Auth.AuthorizationMiddleware' => $config, ``` The default configuration for authorization middleware is: -``` +```php [ 'unauthorizedHandler' => [ 'className' => 'CakeDC/Users.DefaultRedirect', @@ -30,7 +33,7 @@ The default configuration for authorization middleware is: ``` You can check the configuration options available for authorization middleware at the -[official documentation](https://github.com/cakephp/authorization/blob/master/docs/Middleware.md). +[official documentation](https://github.com/cakephp/authorization/blob/master/docs/en/middleware.rst). The `CakeDC/Users.DefaultRedirect` offers additional behavior and config: * If logged user access unauthorized url he is redirected to referer url or '/' if no referer url @@ -41,7 +44,7 @@ The `CakeDC/Users.DefaultRedirect` offers additional behavior and config: You could do the following to set a custom url and flash message: -``` +```php [ 'unauthorizedHandler' => [ 'className' => 'CakeDC/Users.DefaultRedirect', @@ -61,7 +64,7 @@ You could do the following to set a custom url and flash message: ], ``` OR -``` +```php [ 'unauthorizedHandler' => [ 'className' => 'CakeDC/Users.DefaultRedirect', @@ -82,9 +85,10 @@ OR Authorization Component ----------------------- We autoload the authorization component at users controller using the default configuration, -if you don't want the plugin to autoload it, you can do: -``` -Configure::write('Auth.AuthorizationComponent.enabled', false); +if you don't want the plugin to autoload it, you can add this to your config/users.php file: + +```php +'Auth.AuthorizationComponent.enabled' => false, ``` You can check the configuration options available for authorization component at the @@ -100,7 +104,7 @@ default provided. - Create file src/Loader/AppAuthorizationServiceLoader.php -``` +```php \App\Loader\AppAuthorizationServiceLoader::class, ``` diff --git a/Docs/Documentation/Configuration.md b/Docs/Documentation/Configuration.md index 121127cb3..a7118a99b 100644 --- a/Docs/Documentation/Configuration.md +++ b/Docs/Documentation/Configuration.md @@ -6,12 +6,11 @@ Overriding the default configuration For easier configuration, you can specify an array of config files to override the default plugin keys this way: -config/bootstrap.php +Make sure you loaded the plugin and is using a custom config/users.php file at Application::bootstrap ``` // The following configuration setting must be set before loading the Users plugin +$this->addPlugin(\CakeDC\Users\Plugin::class); Configure::write('Users.config', ['users']); -Plugin::load('CakeDC/Users', ['routes' => true, 'bootstrap' => true]); -Configure::write('Users.Social.login', true); //to enable social login ``` Configuration for social login @@ -28,13 +27,14 @@ $ composer require league/oauth1-client:@stable NOTE: twitter uses league/oauth1-client package -config/bootstrap.php -``` -Configure::write('OAuth.providers.facebook.options.clientId', 'YOUR APP ID'); -Configure::write('OAuth.providers.facebook.options.clientSecret', 'YOUR APP SECRET'); +And update your config/users.php file: -Configure::write('OAuth.providers.twitter.options.clientId', 'YOUR APP ID'); -Configure::write('OAuth.providers.twitter.options.clientSecret', 'YOUR APP SECRET'); +```php +'Users.Social.login' => true, +'OAuth.providers.facebook.options.clientId' => 'YOUR APP ID', +'OAuth.providers.facebook.options.clientSecret' => 'YOUR APP SECRET', +'OAuth.providers.twitter.options.clientId' => 'YOUR APP ID', +'OAuth.providers.twitter.options.clientSecret' => 'YOUR APP SECRET', ``` Or use the config override option when loading the plugin (see above) @@ -44,15 +44,18 @@ Additionally you will see you can configure two more keys for each provider: * linkSocialUri (default: /link-social/**provider**), * callbackLinkSocialUri(default: /callback-link-social/**provider**) -Those keys are needed to link an existing user account to a third-party account. **Remember to add the callback to your thrid-party app** +Those keys are needed to link an existing user account to a third-party account. **Remember to add the callback to your thrid-party app** Configuration for reCaptcha --------------------- -``` -Configure::write('Users.reCaptcha.key', 'YOUR RECAPTCHA KEY'); -Configure::write('Users.reCaptcha.secret', 'YOUR RECAPTCHA SECRET'); -Configure::write('Users.reCaptcha.registration', true); //enable on registration -Configure::write('Users.reCaptcha.login', true); //enable on login +To enable reCaptcha you need to register your site at google reCaptcha console +and add this to your config/users.php file: + +```php +'Users.reCaptcha.key' => 'YOUR RECAPTCHA KEY', +'Users.reCaptcha.secret' => 'YOUR RECAPTCHA SECRET', +'Users.reCaptcha.registration' => true, //enable on registration +'Users.reCaptcha.login' => true, //enable on login ``` @@ -70,10 +73,11 @@ and [cakephp/authorization](https://github.com/cakephp/authorization) plugins we into their documentation for more information. Most authentication/authorization configuration is defined at 'Auth' key, for example -if you don't want the plugin to autoload the authorization service, you could do: +if you don't want the plugin to autoload the authorization service, you could add this +to your config/users.php file: ``` -Configure::write('Auth.Authorization.enable', false) +'Auth.Authorization.enable' => false, ``` Interesting Users options and defaults @@ -101,6 +105,8 @@ NOTE: SOME keys were hidden in this doc page, please refer to `vendor/cakedc/use 'ensureActive' => false, // default role name used in registration 'defaultRole' => 'user', + // show verbose error to users + 'showVerboseError' => false, ], 'Tos' => [ // determines if the user should include tos accepted @@ -159,18 +165,20 @@ To learn more about it please check the configurations for [Authentication](Auth ## Using the user's email to login -You need to configure 2 things: +You need to configure 2 things (version 9.0.4): -* Change the Password identifier fields configuration to let it use the email instead of the username for user identify. Add this to your Application class, after CakeDC/Users Plugin is loaded. +* Change the Password identifier fields and the Authenticator for Forms +configuration to let it use the email instead of the username for +user identify. Add this to your config/users.php: ```php - $identifiers = Configure::read('Auth.Identifiers'); - $identifiers['Authentication.Password']['fields']['username'] = 'email'; - Configure::write('Auth.Identifiers', $identifiers); +'Auth.Identifiers.Password.fields.username' => 'email', +'Auth.Authenticators.Form.fields.username' => 'email', +'Auth.Authenticators.Cookie.fields.username' => 'email', ``` -* Override the login.php template to change the Form->control to "email". -Add (or copy from the [/templates/Users/login.php](../../templates/Users/login.php)) the file login.php to path /templates/plugin/CakeDC/Users/Users/login.php +* Override the login.php template to change the Form->control to "email". +Add (or copy from the [/templates/Users/login.php](../../templates/Users/login.php)) the file login.php to path /templates/plugin/CakeDC/Users/Users/login.php and ensure it has the following content ```php @@ -210,3 +218,37 @@ Check https://book.cakephp.org/4/en/core-libraries/internationalization-and-loca for more details about how the PO files should be managed in your application. We've included an updated POT file with all the `Users` domain keys for your customization. + +Password Hasher customization +----------------------------- + +Override the `Auth.Identifiers.Password` key in configuration adding a `passwordHasher` key https://book.cakephp.org/authentication/2/en/password-hashers.html#upgrading-hashing-algorithms + +For example: + +```php + 'Auth.Identifiers' => [ + 'Password' => [ + 'className' => 'Authentication.Password', + 'fields' => [ + 'username' => ['username', 'email'], + 'password' => 'password', + ], + 'resolver' => [ + 'className' => 'Authentication.Orm', + 'finder' => 'active', + ], + 'passwordHasher' => [ + 'className' => 'Authentication.Fallback', + 'hashers' => [ + 'Authentication.Default', + [ + 'className' => 'Authentication.Legacy', + 'hashType' => 'md5', + 'salt' => false, // turn off default usage of salt + ], + ], + ], + ], + ], +``` diff --git a/Docs/Documentation/Events.md b/Docs/Documentation/Events.md index d407316aa..61fcee79d 100644 --- a/Docs/Documentation/Events.md +++ b/Docs/Documentation/Events.md @@ -14,23 +14,555 @@ The events in this plugin follow these conventions `. 'beforeLogout', + ]; + } + + /** + * @param \Cake\Event\Event $event + */ + public function beforeLogout(\Cake\Event\Event $event) + { + $user = $event->getData('user'); + $controller = $event->getSubject(); + + //your custom logic + Cache::delete('dashboard_data_user_' . $user['id']); + $controller->Flash->succes(__('Some message if you want')); + + //If you want to ignore the logout logic you can, just set an url array as result to use as redirect + //$event->setResult(['plugin' => false, 'controller' => 'Page', 'action' => 'seeYouSoon']); + } +} +``` +- Add this at the end of your method Application::bootstrap if you have NOT done before. +```php +$this->getEventManager()->on(new \App\Event\UsersListener()); +``` + +I want to add custom logic before user register +--------------------------------------------- +When adding a custom logic to execute before user register you +have access to 'usersTable', 'userEntity' and 'options' keys in the event +object and the controller object. +You can also populate the new user entity or stop the register process. + +- Create or update file src/Event/UsersListener.php: +```php + 'beforeRegister', + ]; + } + + /** + * @param \Cake\Event\Event $event + */ + public function beforeRegister(\Cake\Event\Event $event) + { + $controller = $event->getSubject(); + $user = $event->getData('userEntity'); + $table = $event->getData('usersTable'); + $options = $event->getData('options'); + + //Or custom logic + $controller->Flash->succes(__('Some message if you want')); + + //When you set an entity as result a part of register logic is skipped (ex: reCaptcha) + $newUser = $table->newEntity([ + 'username' => 'forceEventRegister', + 'email' => 'eventregister@example.com', + 'password' => 'password', + 'active' => true, + 'tos' => true, + ]); + // + $event->setResult($newUser); + //If you want to stop registration use + //$event->stopPropagation(); + //$event->setResult(['plugin' => false, 'controller' => 'Somewhere', 'action' => 'toRedirect']); + } +} + +``` +- Add this at the end of your method Application::bootstrap if you have NOT done before. +```php +$this->getEventManager()->on(new \App\Event\UsersListener()); +``` + +I want to add custom logic before linking social account +-------------------------------------------------------- +When adding a custom logic to execute before linking social account you +have access to 'location' and 'request' keys in the event object and +the controller object. + +- Create or update file src/Event/UsersListener.php: +```php + 'beforeSocialLoginRedirect', + ]; + } + + /** + * @param \Cake\Event\Event $event + */ + public function beforeSocialLoginRedirect(\Cake\Event\Event $event) + { + $controller = $event->getSubject(); + $location = $event->getData('location'); + $request = $event->getData('request'); + + //your custom logic + } +} + +``` +- Add this at the end of your method Application::bootstrap if you have NOT done before. +```php +$this->getEventManager()->on(new \App\Event\UsersListener()); +``` + +I want to add custom logic before creating social account +--------------------------------------------------------- +When adding a custom logic to execute before creating social account you +have access to 'userEntity' and 'data' keys in the event object and +the social behavior object. + +You can also set a new user entity object as result. + +- Create or update file src/Event/UsersListener.php: +```php + 'beforeSocialLoginRedirect', + ]; + } + + /** + * @param \Cake\Event\Event $event + */ + public function beforeSocialLoginRedirect(\Cake\Event\Event $event) + { + $userEntity = $event->getData('userEntity'); + $socialData = $event->getData('data'); + + //your custom logic + + //If you want to use another entity use this + //$event->setResult($anotherUserEntity); + } +} +``` +- Add this at the end of your method Application::bootstrap if you have NOT done before. +```php +$this->getEventManager()->on(new \App\Event\UsersListener()); +``` + +I want to add custom logic after user login +------------------------------------------- +When adding a custom logic to execute after user login you +have access to user data. You can also set an array as result to +perform a custom redirect. + +- Create or update file src/Event/UsersListener.php: +```php + 'afterLogin', + ]; + } /** - * beforeRegister event + * @param \Cake\Event\Event $event */ - public function eventRegister() + public function afterLogin(\Cake\Event\Event $event) { - $this->eventManager()->on(Plugin::EVENT_BEFORE_REGISTER, function ($event) { - //the callback function should return the user data array to force register - return $event->data['usersTable']->newEntity([ - 'username' => 'forceEventRegister', - 'email' => 'eventregister@example.com', - 'password' => 'password', - 'active' => true, - 'tos' => true, - ]); - }); - $this->register(); - $this->render('register'); + $user = $event->getData('user'); + //your custom logic + //$this->loadModel('SomeOptionalUserLogs')->newLogin($user); + + //If you want to use a custom redirect + $event->setResult([ + 'plugin' => false, + 'controller' => 'Dashboard', + 'action' => 'home', + ]); } +} +``` +- Add this at the end of your method Application::bootstrap if you have NOT done before. +```php +$this->getEventManager()->on(new \App\Event\UsersListener()); +``` + +I want to add custom logic after user logout +--------------------------------------------- +When adding a custom logic to execute after user logout you +have access to user data and the controller object. You can also +set an array as result to perform a custom redirect. + +- Create or update file src/Event/UsersListener.php: +```php + 'afterLogout', + ]; + } + + /** + * @param \Cake\Event\Event $event + */ + public function afterLogout(\Cake\Event\Event $event) + { + $user = $event->getData('user'); + $controller = $event->getSubject(); + //your custom logic + + //If you want to use a custom redirect + $event->setResult([ + 'plugin' => false, + 'controller' => 'Pages', + 'action' => 'thankYou', + ]); + } +} + +``` +- Add this at the end of your method Application::bootstrap if you have NOT done before. +```php +$this->getEventManager()->on(new \App\Event\UsersListener()); +``` + +I want to add custom logic after user register +---------------------------------------------- +When adding a custom logic to execute after user register you +have access to user data and the controller object. You can also +set a custom http response as result to render a different content +or perform a custom redirect. + +- Create or update file src/Event/UsersListener.php: +```php + 'afterRegister', + ]; + } + + /** + * @param \Cake\Event\Event $event + */ + public function afterRegister(\Cake\Event\Event $event) + { + $user = $event->getData('user'); + $controller = $event->getSubject(); + //your custom logic + + //If you want to use a custom response to render a json. + $response = $controller->getResponse()->withStringBody(json_encode(['success' => true, 'id' => $user['id']])); + $event->setResult($response); + + //or if you want to use a custom redirect. + $response = $controller->getResponse()->withLocation("/some/page"); + $event->setResult($response); + } +} + +``` +- Add this at the end of your method Application::bootstrap if you have NOT done before. +```php +$this->getEventManager()->on(new \App\Event\UsersListener()); +``` + +I want to add custom logic after user changed the password +---------------------------------------------------------- +When adding a custom logic to execute after user change the password +you have access to some user data and the controller object. You can also +set an array as result to perform a custom redirect. + +- Create or update file src/Event/UsersListener.php: +```php + 'afterChangePassword', + ]; + } + + /** + * @param \Cake\Event\Event $event + */ + public function afterChangePassword(\Cake\Event\Event $event) + { + $user = $event->getData('user'); + $controller = $event->getSubject(); + //your custom logic + + //If you want to use a custom redirect + $event->setResult([ + 'plugin' => false, + 'controller' => 'Pages', + 'action' => 'infoPassword', + ]); + } +} + +``` +- Add this at the end of your method Application::bootstrap if you have NOT done before. +```php +$this->getEventManager()->on(new \App\Event\UsersListener()); +``` + + +I want to add custom logic after sending the token for user validation +---------------------------------------------------------------------- +When adding a custom logic to execute after sending the token for user +validation you can also set an array as result to perform a custom redirect. + +- Create or update file src/Event/UsersListener.php: +```php + 'afterResendTokenValidation', + ]; + } + + /** + * @param \Cake\Event\Event $event + */ + public function afterResendTokenValidation(\Cake\Event\Event $event) + { + $controller = $event->getSubject(); + //your custom logic + + //If you want to use a custom redirect + $event->setResult([ + 'plugin' => false, + 'controller' => 'Pages', + 'action' => 'infoValidation', + ]); + } +} + +``` +- Add this at the end of your method Application::bootstrap if you have NOT done before. +```php +$this->getEventManager()->on(new \App\Event\UsersListener()); +``` + +I want to add custom logic after user email is validated +-------------------------------------------------------- +When adding a custom logic to execute after user email is validate +you have access to some user data and the controller object. You can also +set an array as result to perform a custom redirect. + +- Create or update file src/Event/UsersListener.php: +```php + 'afterEmailTokenValidation', + ]; + } + + /** + * @param \Cake\Event\Event $event + */ + public function afterEmailTokenValidation(\Cake\Event\Event $event) + { + $user = $event->getData('user'); + $controller = $event->getSubject(); + //your custom logic + + //If you want to use a custom redirect + $event->setResult([ + 'plugin' => false, + 'controller' => 'Pages', + 'action' => 'infoPassword', + ]); + } +} + +``` +- Add this at the end of your method Application::bootstrap if you have NOT done before. +```php +$this->getEventManager()->on(new \App\Event\UsersListener()); +``` + +I want to add custom logic after user email is validated to autologin user +-------------------------------------------------------------------------- +This is how you can autologin the user after email is validate: + +- Create or update file src/Event/UsersListener.php: +```php + 'afterEmailTokenValidation', + ]; + } + + /** + * @param \Cake\Event\Event $event + */ + public function afterEmailTokenValidation(\Cake\Event\Event $event) + { + $table = $this->loadModel('Users'); + $userData = $event->getData('user'); + $user = $table->get($userData['id']); + $this->Authentication->setIdentity($user); + } +} + +``` +- Add this at the end of your method Application::bootstrap if you have NOT done before. +```php +$this->getEventManager()->on(new \App\Event\UsersListener()); +``` diff --git a/Docs/Documentation/Extending-the-Plugin.md b/Docs/Documentation/Extending-the-Plugin.md index e205ac8df..4f8cfe77e 100644 --- a/Docs/Documentation/Extending-the-Plugin.md +++ b/Docs/Documentation/Extending-the-Plugin.md @@ -123,7 +123,7 @@ class MyUsersController extends AppController { use LoginTrait; use RegisterTrait; - + /** * Initialize * @@ -156,6 +156,9 @@ needed to setup correct url/route for authentication. // ... ``` +**You also need to update permissions rules in your file config/permissions.php +to match the new controller.** + Note you'll need to **copy the Plugin templates** you need into your project templates/MyUsers/[action].php You may also need to load some helpers in your AppView: @@ -212,7 +215,7 @@ https://book.cakephp.org/4/en/plugins.html#overriding-plugin-templates-from-insi Updating the Emails ------------------- -Extend the `\CakeDC\Users\Mailer\UsersMailer` class and override the email configuration to change the way the +Extend the `\CakeDC\Users\Mailer\UsersMailer` class and override the email configuration to change the way the emails are sent by the Plugin. We currently have: * validation, sent with a link to validate new users registered * resetPassword, sent with a link to access the reset password feature @@ -236,8 +239,11 @@ class MyUsersMailer extends UsersMailer } } ``` -* Configure the Plugin to use this new mailer class in bootstrap or users.php -`Configure::write('Users.Email.mailerClass', \App\Mailer\MyUsersMailer::class);` +* Configure the plugin to use this new mailer class, add this in your config/users.php file: + +```php + 'Users.Email.mailerClass' => \App\Mailer\MyUsersMailer::class, +``` * Create the file `templates/email/text/custom_template_in_app_namespace.php` with your custom contents. Note you can also prepare an html version of the file, diff --git a/Docs/Documentation/Installation.md b/Docs/Documentation/Installation.md index 4171bee7c..f9d8d8e8d 100644 --- a/Docs/Documentation/Installation.md +++ b/Docs/Documentation/Installation.md @@ -2,7 +2,7 @@ Installation ============ Composer ------- +-------- ``` composer require cakedc/users @@ -21,10 +21,6 @@ composer require league/oauth1-client:@stable NOTE: you'll need to enable social login if you want to use it, social login is disabled by default. Check the [Configuration](Configuration.md#configuration-for-social-login) page for more details. -``` -Configure::write('Users.Social.login', true); //to enable social login -``` - If you want to use reCaptcha features... ``` @@ -40,14 +36,14 @@ If you want to use Google Authenticator features... composer require robthree/twofactorauth:"^1.5.2" ``` -NOTE: you'll need to enable `OneTimePasswordAuthenticator.login` +NOTE: you'll need to enable `OneTimePasswordAuthenticator.login` in your config/users.php file: -``` -Configure::write('OneTimePasswordAuthenticator.login', true); +```php +'OneTimePasswordAuthenticator.login' => true, ``` Load the Plugin ------------ +--------------- Ensure the Users Plugin is loaded in your src/Application.php file @@ -65,6 +61,36 @@ Ensure the Users Plugin is loaded in your src/Application.php file } ``` +**Important note: The plugin loads authentication and authorization plugin and +uses RequestAuthorizationMiddleware with Rbac|Superuser policy you +should not load then manually** + +Creating config files +--------------------- +You need to create the file config/users.php to configure the plugin. This documentation +assumes that you will create this file. + +Example config/users.php + +```php + true, +]; +``` + +***The plugin loads authentication and authorization plugins by default, +to be able to access your pages you NEED to have defined rules at the +file config/permissions.php. +You can copy the one from the plugin and add your permissions rules.*** + +```shell +cd {project_dir} +cp vendor/cakedc/users/config/permissions.php config/permissions.php +``` +[Go to permission documentation for more information.](./Permissions.md) + + Creating Required Tables ------------------------ If you want to use the Users tables to store your users and social accounts: @@ -84,18 +110,19 @@ bin/cake users addSuperuser ``` Customization ----------- +------------- -Application::bootstrap +First, make sure to set the config `Users.config` at Application::bootstrap ``` $this->addPlugin(\CakeDC\Users\Plugin::class); Configure::write('Users.config', ['users']); -Configure::write('Users.Social.login', true); //to enable social login ``` -If you want to use social login, then in your config/users.php -``` +And update your config/users.php file, for example if you want to use social login: +```php + true, 'OAuth.providers.facebook.options.clientId' => 'YOUR APP ID', 'OAuth.providers.facebook.options.clientSecret' => 'YOUR APP SECRET', 'OAuth.providers.twitter.options.clientId' => 'YOUR APP ID', @@ -103,7 +130,7 @@ return [ //etc ]; ``` -IMPORTANT: Remember you'll need to configure your social login application **callback url** to use the provider specific endpoint, for example: +IMPORTANT: Remember you'll need to configure your social login application **callback url** to use the provider specific endpoint, for example: * Facebook App Callback URL --> `http://yourdomain.com/auth/facebook` * Twitter App Callback URL --> `http://yourdomain.com/auth/twitter` * Google App Callback URL --> `http://yourdomain.com/auth/google` diff --git a/Docs/Documentation/InterceptLoginAction.md b/Docs/Documentation/InterceptLoginAction.md index a4bc96336..2e27b868d 100644 --- a/Docs/Documentation/InterceptLoginAction.md +++ b/Docs/Documentation/InterceptLoginAction.md @@ -6,7 +6,7 @@ some specific login, like redirect user to another url or set user data. A simple way to intercept the login action is by creating a custom middleware, the following example shows how to set user data and redirect to anothe url. -``` +```php session(['Auth.User' => $userData]); +``` + +with + +``` +$this->session(['Auth' => new \CakeDC\Users\Model\Entity\User($userData)]); +``` \ No newline at end of file diff --git a/Docs/Documentation/Permissions.md b/Docs/Documentation/Permissions.md new file mode 100644 index 000000000..d2b55f20f --- /dev/null +++ b/Docs/Documentation/Permissions.md @@ -0,0 +1,183 @@ +Permissions +=========== +The plugin is setup to perform permissions check for all requests using +Superuser and Rbac policies. + +Superuser policy allow the superuser to access any page. + +The Rbac policy allows you to define a list of rules at config/permissions.php +to perform checks based on request information (prefix, plugin, controller, action, etc) +and user data. + +You can find the permission rule syntax at [CakeDC/auth documentation.](https://github.com/CakeDC/auth/blob/master/Docs/Documentation/Rbac.md#permission-rules-syntax) + +I want to allow access to public actions (non-logged user) +---------------------------------------------------------- +To allow access to public actions (that does not requires a looged) we need to include a new rule at config/permissions.php +using the 'bypassAuth' key. + +```php + [ + //...... all other rules + [ + 'controller' => 'Pages', + 'action' => ['home', 'contact', 'projects'] + 'bypassAuth' => true, + ], + ], +]; +``` + +I want to allow access to one specific action +--------------------------------------------- +To allow access to specific action we need to include a new rule at config/permissions.php + +- Path: /{controler}/{action} +```php + [ + //...... all other rules + [ + //Allow user, manager and author roles to access /books + 'role' => ['user', 'manager', 'author'], + 'controller' => 'Books', + 'action' => 'index', + ], + [ + //Allow user to access /dashboard/home + 'role' => 'user', + 'controller' => 'Dashbord', + 'action' => 'home', + ], + [ + //Allow user to access /articles, /articles/add and /article/edit + 'role' => ['manager'], + 'controller' => 'Articles', + 'action' => ['index', 'add', 'edit'], + ], + ], +]; +``` + +- Path: /{plugin}/{prefix}/{controler}/{action} +```php + [ + //...... all other rules + [ + //Allow user to access /reports/admin/categories + 'plugin' => 'Reports', + 'prefix' => 'Admin', + 'role' => ['manager'], + 'controller' => 'Categories', + 'action' => ['index'], + ], + ], +]; +``` + +I want to allow access to all actions from one controller +--------------------------------------------------------- +To allow access to specific all actions from one controller we need to include a new rule at config/permissions.php +using the value '*' for 'action' key. + +```php + [ + //...... all other rules + [ + //Allow user, manager and author roles to access any action from books controller + 'role' => ['user', 'manager', 'author'], + 'controller' => 'Books', + 'action' => '*', + ], + ] +]; +``` + +I want to allow access to all controllers from one prefix +--------------------------------------------- +To allow access to specific to all pages from one prefix we need to include a new rule at config/permissions.php +using the value '*' for 'plugin', 'controller' and 'action' keys. + +```php + [ + //...... all other rules + [ + //Allow user, manager and author roles to access any action from books controller + 'role' => ['user', 'manager', 'author'], + 'plugin' => '*', + 'prefix' => 'Admin', + 'controller' => '*', + 'action' => '*', + ], + ], +]; +``` + +I want to allow access to entity owned by the user +-------------------------------------------------- +To allow access to entity owned by the user we need to include a new rule +at config/permissions.php using the 'allowed' key. + +```php + [ + //...... all other rules + [ + // + 'role' => 'user', + 'controller' => 'Articles', + 'action' => ['edit'] + 'allowed' => new \CakeDC\Auth\Rbac\Rules\Owner([ + 'table' => 'Articles', + 'id' => 'id', + 'ownerForeignKey' => 'owner_id' + ]), + ], + ], +]; +``` + +[For more information check owner rule documentation](https://github.com/CakeDC/auth/blob/6.next-cake4/Docs/Documentation/OwnerRule.md) + + +I want to allow access to action using a custom logic +---------------------------------------------------- +Permission rule can have a custom callback. Adde the rule at config/permissions.php using the 'allowed' key. + +```php + [ + //...... all other rules + [ + // + 'role' => 'user', + 'controller' => 'Posts', + 'action' => ['edit'] + 'allowed' => function (array $user, $role, \Cake\Http\ServerRequest $request) { + $postId = \Cake\Utility\Hash::get($request->params, 'pass.0'); + $post = \Cake\ORM\TableRegistry::get('Posts')->get($postId); + $userId = $user['id']; + if (!empty($post->user_id) && !empty($userId)) { + return $post->user_id === $userId; + } + return false; + } + ], + ], +]; +``` + +[For more information check CakeDC/Auth documentation](https://github.com/CakeDC/auth/blob/6.next-cake4/Docs/Documentation/Rbac.md#permission-callbacks) + + diff --git a/Docs/Documentation/SocialAuthentication.md b/Docs/Documentation/SocialAuthentication.md index 7ec4211f2..f74b36e7a 100644 --- a/Docs/Documentation/SocialAuthentication.md +++ b/Docs/Documentation/SocialAuthentication.md @@ -12,37 +12,40 @@ We currently support the following providers to perform login as well as to link Please [contact us](https://cakedc.com/contact) if you need to support another provider. -The main source code for social integration is provided by ['CakeDC/auth' plugin](https://github.com/cakedc/auth) +The main source code for social integration is provided by ['CakeDC/auth' plugin](https://github.com/cakedc/auth) Setup ----- +By default social login is disabled, to enable you need to create the +Facebook/Twitter applications you want to use and update your file config/users.php with: -Create the Facebook/Twitter applications you want to use and setup the configuration like this: - -Config/bootstrap.php +```php +//This enable social login (authentication) +'Users.Social.login' => true, +//This is the required config to setup facebook. +'OAuth.providers.facebook.options.clientId', 'YOUR APP ID'; +'OAuth.providers.facebook.options.clientSecret', 'YOUR APP SECRET'; +//This is the required config to setup twitter +'OAuth.providers.twitter.options.clientId', 'YOUR APP ID'; +'OAuth.providers.twitter.options.clientSecret', 'YOUR APP SECRET'; ``` -Configure::write('OAuth.providers.facebook.options.clientId', 'YOUR APP ID'); -Configure::write('OAuth.providers.facebook.options.clientSecret', 'YOUR APP SECRET'); +Check optional configs at [config/users.php](./../../config/users.php) inside 'OAuth' key -Configure::write('OAuth.providers.twitter.options.clientId', 'YOUR APP ID'); -Configure::write('OAuth.providers.twitter.options.clientSecret', 'YOUR APP SECRET'); -``` -You can also change the default settings for social authenticate: +You can also change the default settings for social authenticate in your config/users.php file: -``` -Configure::write('Users', [ - 'Email' => [ +```php + 'Users.Email' => [ //determines if the user should include email 'required' => true, //determines if registration workflow includes email validation 'validate' => true, ], - 'Social' => [ + 'Users.Social' => [ //enable social login 'login' => false, ], - 'Key' => [ + 'Users.Key' => [ 'Session' => [ //session key to store the social auth data 'social' => 'Users.social', @@ -56,7 +59,6 @@ Configure::write('Users', [ 'socialEmail' => 'info.email', ], ], -]); ``` If email is required and the social network does not return the user email then the user will be required to input the email. Additionally, validation could be enabled, in that case the user will be asked to validate the email before be able to login. There are some cases where the email address already exists onto database, if so, the user will receive an email and will be asked to validate the social account in the app. It is important to take into account that the user account itself will remain active and accessible by other ways (other social network account or username/password). @@ -65,8 +67,8 @@ In most situations you would not need to change any Oauth setting besides applic For new facebook aps you must use the graphApiVersion 2.8 or greater: -``` -Configure::write('OAuth.providers.facebook.options.graphApiVersion', 'v2.8'); +```php +'OAuth.providers.facebook.options.graphApiVersion' => 'v2.8', ``` User Helper @@ -75,7 +77,7 @@ User Helper You can use the helper included with the plugin to create Facebook/Twitter buttons: In templates -``` +```php $this->User->facebookLogin(); $this->User->twitterLogin(); @@ -92,7 +94,7 @@ In your customized users table, add the SocialBehavior with the following config ```php $this->addBehavior('CakeDC/Users.Social', [ - 'username' => 'email' + 'username' => 'email' ]); ``` Or if you extend the users table, the behavior is already loaded, so just configure it with: @@ -122,12 +124,11 @@ Social Indentifier ------------------ The social identifier "CakeDC/Users.Social", works with data provider by both social authenticator, it is responsible of finding or creating a user registry for the social user data request. -By default it'll fetch user data with finder 'all', but you can use a custom one. Add this to your -Application class, after CakeDC/Users Plugin is loaded. -``` - $identifiers = Configure::read('Auth.Identifiers'); - $identifiers['CakeDC/Users.Social']['authFinder'] = 'customSocialAuth'; - Configure::write('Auth.Identifiers', $identifiers); +By default, it'll fetch user data with finder 'all', but you can use a custom one. Add this to your +config/users.php: + +```php +'Auth.Identifiers.Social.authFinder' => 'customSocialAuth', ``` @@ -138,13 +139,13 @@ service to redirects user to an internal page or show an authentication error. I There are two custom messages (Auth.SocialLoginFailure.messages) and one default message (Auth.SocialLoginFailure.defaultMessage). -To use a custom component to handle the login, do: +To use a custom component to handle the login add this to your config/users.php file: +```php +'Auth.SocialLoginFailure.component' => 'MyLoginA', ``` -Configure::write('Auth.SocialLoginFailure.component', 'MyLoginA'); -``` The default configuration is: -``` +```php [ ... 'Auth' => [ @@ -167,4 +168,4 @@ The default configuration is: ... ] ] -``` +``` diff --git a/Docs/Documentation/Translations.md b/Docs/Documentation/Translations.md index 41abc4e08..d9cc74def 100644 --- a/Docs/Documentation/Translations.md +++ b/Docs/Documentation/Translations.md @@ -10,8 +10,9 @@ The Plugin is translated into several languages: * Polish (pl) by @joulbex * Hungarian (hu_HU) by @rrd108 * Italian (it) by @arturmamedov -* Turkish (tr_TR) by @sayinserdar +* Turkish (tr_TR) by @sayinserdar +* Ukrainian (uk) by @yarkm13 -**Note:** To overwrite the plugin translations, create a file inside your project 'src/Locale/{$lang}/' folder, with the name 'Users.po' and add the strings with the new translations. +**Note:** To overwrite the plugin translations, create a file inside your project 'resources/locales//{$lang}/' folder, with the name 'users.po' and add the strings with the new translations. Remember to clean the translations cache! diff --git a/Docs/Documentation/Two-Factor-Authenticator.md b/Docs/Documentation/Two-Factor-Authenticator.md index 0ee975fc8..022445a01 100644 --- a/Docs/Documentation/Two-Factor-Authenticator.md +++ b/Docs/Documentation/Two-Factor-Authenticator.md @@ -1,28 +1,46 @@ Two Factor Authenticator =============================== +The plugin offers an easy way to integrate OTP Two-Factor authentication +in the users login flow of your application. -Installation ------------- -To enable this feature you need to + +Installation Requirement +------------------------ +Before you enable the feature you need to run ``` composer require robthree/twofactorauth ``` -Setup ------ +By default the feature is disabled. + +Enabling +-------- -Enable one-time password authenticator in your bootstrap.php file: +First install robthree/twofactorauth using composer: -Config/bootstrap.php ``` -Configure::write('OneTimePasswordAuthenticator.login', true); +composer require robthree/twofactorauth +``` + +Then add this in your config/users.php file: + +```php + 'OneTimePasswordAuthenticator.login' => true, +``` + +Disabling +--------- +You can disable it by adding this in your config/users.php file: + +```php + 'OneTimePasswordAuthenticator.login' => false, ``` How does it work ---------------- When the user log-in, he is requested (image 1) to inform the current validation -code for your site in Google Authentation app (image 2), if this is the first +code for your site in Google Authentation app (image 2), if this is the first time he access he need to add your site to Google Authentation by reading the QR code shown (image 1). diff --git a/Docs/Documentation/Yubico-U2F.md b/Docs/Documentation/Yubico-U2F.md index bf398013b..73511f16c 100644 --- a/Docs/Documentation/Yubico-U2F.md +++ b/Docs/Documentation/Yubico-U2F.md @@ -1,22 +1,30 @@ YubicoKey U2F ============= -Installation ------------- -To enable this feature you need to +The plugin offers an easy way to integrate U2F in the users login flow +of your application. + +Enabling +-------- + +First install yubico/u2flib-server using composer: ``` composer require yubico/u2flib-server:^1.0 ``` -Setup ------ - -Enable it in your bootstrap.php file: +Then add this in your config/users.php file: -Config/bootstrap.php +```php + 'U2f.enabled' => true, ``` -Configure::write('U2f.enabled', true); + +Disabling +--------- +You can disable it by adding this in your config/users.php file: + +```php + 'U2f.enabled' => false, ``` How does it work diff --git a/Docs/Home.md b/Docs/Home.md index 5e493cb19..4223b0e82 100644 --- a/Docs/Home.md +++ b/Docs/Home.md @@ -18,14 +18,228 @@ Documentation * [SimpleRbacAuthorize](https://github.com/CakeDC/auth/blob/master/Docs/Documentation/SimpleRbacAuthorize.md) * [SuperuserAuthorize](https://github.com/CakeDC/auth/blob/master/Docs/Documentation/SuperuserAuthorize.md) * [Intercept Login Action](Documentation/InterceptLoginAction.md) -* [Social Authentication](Documentation/SocialAuthenticate.md) +* [Social Authentication](Documentation/SocialAuthentication.md) * [Google Authenticator](Documentation/Two-Factor-Authenticator.md) * [Yubico U2F](Documentation/Yubico-U2F.md) * [UserHelper](Documentation/UserHelper.md) +* [AuthLinkHelper](Documentation/AuthLinkHelper.md) * [Events](Documentation/Events.md) * [Extending the Plugin](Documentation/Extending-the-Plugin.md) * [Translations](Documentation/Translations.md) +I want to +--------- +* extend the + * [model](Documentation/Extending-the-Plugin.md#extending-the-model-tableentity) + * [controller](Documentation/Extending-the-Plugin.md#extending-the-controller) + * [templates](Documentation/Extending-the-Plugin.md#updating-the-templates) + +* enable or disable + * + email validation + + Add this to your config/users.php file to disable email validation + + ```php + 'Users.Email.validate' => false, + ``` + or this to enable (default) + + ```php + 'Users.Email.validate' => true, + ``` + + * + registration + + Add this to your config/users.php file to disable registration + + ```php + 'Users.Registration.active' => false, + ``` + or this to enable (default) + + ```php + 'Users.Registration.active' => true, + ``` + + * + reCaptcha on registration + + To enable reCaptcha you need to register your site at google reCaptcha console + and add this to your config/users.php file to enable on registration: + + ```php + 'Users.reCaptcha.key' => 'YOUR RECAPTCHA KEY', + 'Users.reCaptcha.secret' => 'YOUR RECAPTCHA SECRET', + 'Users.reCaptcha.registration' => true, + ``` + To disable (default) add this to your config/users.php + + ```php + 'Users.reCaptcha.registration' => false, + ``` + + * + reCaptcha on login + + To enable reCaptcha you need to register your site at google reCaptcha console + and add this to your config/users.php file to enable on login: + + ```php + 'Users.reCaptcha.key' => 'YOUR RECAPTCHA KEY', + 'Users.reCaptcha.secret' => 'YOUR RECAPTCHA SECRET', + 'Users.reCaptcha.login' => true, + ``` + To disable (default) add this to your config/users.php + + ```php + 'Users.reCaptcha.login' => false, + ``` + + * [social login](./Documentation/SocialAuthentication.md#setup) + * [OTP Two-factor authenticator](./Documentation/Two-Factor-Authenticator.md) + * [Yubico Key U2F Two-factor authenticator](./Documentation/Yubico-U2F.md) + * + Authentication component + + Add this to your config/users.php file to autoload the component (default): + + ```php + 'Auth.AuthenticationComponent.load' => true, + ``` + + To not autoload add this to your config/users.php + + ```php + 'Auth.AuthenticationComponent.load' => false, + ``` + + * + Authorization component + + Add this to your config/users.php file to autoload the component (default): + + ```php + 'Auth.AuthorizationComponent.enabled' => true, + ``` + + To not autoload add this to your config/users.php + + ```php + 'Auth.AuthorizationComponent.enabled' => false, + ``` + + + * + TOS validation + + Add this to your config/users.php file to enable (default): + + ```php + 'Users.Tos.required' => true, + ``` + + To disable add this to your config/users.php + + ```php + 'Users.Tos.required' => false, + ``` + + + * + remember me + + Add this to your config/users.php file to enable (default): + + ```php + 'Users.RememberMe.active' => true, + ``` + + To disable add this to your config/users.php + + ```php + 'Users.RememberMe.active' => false, + ``` + + +- allow access to + - [public actions (non-logged user)](./Documentation/Permissions.md#i-want-to-allow-access-to-public-actions-non-logged-user) + - [one specific action](./Documentation/Permissions.md#i-want-to-allow-access-to-one-specific-action) + - [all actions from one controller](./Documentation/Permissions.md#i-want-to-allow-access-to-all-actions-from-one-controller) + - [all controllers from one prefix](./Documentation/Permissions.md#i-want-to-allow-access-to-all-controllers-from-one-prefix) + - [entity owned by the user](./Documentation/Permissions.md#i-want-to-allow-access-to-entity-owned-by-the-user) + - [action using a custom logic](./Documentation/Permissions.md#i-want-to-allow-access-to-action-using-a-custom-logic) + +- customize my login page to + - + use my template + Copy the login file from `{project_dir}/vendor/cakedc/users/templates/Users/` + to `{project_dir}/templates/plugin/CakeDC/Users/Users`. + + + - + use a custom finder + First add this to your config/users.php: + + ``` + 'Auth.Identifiers.Password.resolver.finder' => 'myFinderName', + 'Auth.Identifiers.Social.authFinder' => 'myFinderName', + 'Auth.Identifiers.Token.resolver.finder' => 'myFinderName', + ``` + Important: You must have extended the model, see how to at [Extending the Plugin](Documentation/Extending-the-Plugin.md) + + + - + use a custom redirect url + To use a custom redirect url on login add this to your config/users.php: + + ``` + 'Auth.AuthenticationComponent.loginRedirect' => '/some/url/', + ``` + or + ``` + 'Auth.AuthenticationComponent.loginRedirect' => ['plugin' => false, 'controller' => 'Example', 'action' => 'home'], + ``` + Important: when using array you should pass `'plugin' => false,` to match your app controller. + + + - + enable|disable reCaptcha + + To enable reCaptcha you need to register your site at google reCaptcha console + and add this to your config/users.php file to enable on login: + + ```php + 'Users.reCaptcha.login' => true, + 'Users.reCaptcha.key' => 'YOUR RECAPTCHA KEY', + 'Users.reCaptcha.secret' => 'YOUR RECAPTCHA SECRET', + ``` + To disable (default) add this to your config/users.php + ```php + 'Users.reCaptcha.login' => false, + ``` + + - [use user's email to login](./Documentation/Configuration.md#using-the-users-email-to-login) + - [override the password hasher](./Documentation/Configuration.md#password-hasher-customization) + +- add custom logic before + - [user logout](./Documentation/Events.md#i-want-to-add-custom-logic-before-user-logout) + - [user register](./Documentation/Events.md#i-want-to-add-custom-logic-before-user-register) + - [linking social account](./Documentation/Events.md#i-want-to-add-custom-logic-before-linking-social-account) + - [creating social account](./Documentation/Events.md#i-want-to-add-custom-logic-before-creating-social-account) + +- add custom logic after + - [user login](./Documentation/Events.md#i-want-to-add-custom-logic-after-user-login) + - [user logout](./Documentation/Events.md#i-want-to-add-custom-logic-after-user-logout) + - [user register](./Documentation/Events.md#i-want-to-add-custom-logic-after-user-register) + - [user changed the password](./Documentation/Events.md#i-want-to-add-custom-logic-after-user-changed-the-password) + - [sending the token for user validation](./Documentation/Events.md#i-want-to-add-custom-logic-after-sending-the-token-for-user-validation) + - [user email is validated](./Documentation/Events.md#i-want-to-add-custom-logic-after-user-email-is-validated) + - [user email is validated to autologin user](./Documentation/Events.md#i-want-to-add-custom-logic-after-user-email-is-validated-to-autologin-user) + - [intercept login action](./Documentation/InterceptLoginAction.md) + + Migration guides ---------------- diff --git a/README.md b/README.md index f3c950233..fbd5af5f4 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ CakeDC Users Plugin =================== -[](http://travis-ci.org/CakeDC/users) +[](https://github.com/CakeDC/users/actions?query=workflow%3ACI+branch%3Amaster) [](https://codecov.io/gh/CakeDC/users) [](https://packagist.org/packages/CakeDC/users) [](https://packagist.org/packages/CakeDC/users) @@ -12,19 +12,19 @@ Versions and branches | CakePHP | CakeDC Users Plugin | Tag | Notes | | :-------------: | :------------------------: | :--: | :---- | -| ^4.0 | [master](https://github.com/cakedc/users/tree/master) | 9.0.2 | stable | -| ^4.0 | [9.0](https://github.com/cakedc/users/tree/9.next) | 9.0.2 | stable | +| ^4.3 | [master](https://github.com/cakedc/users/tree/master) | 11.0.0 | stable | +| ^4.3 | [11.0](https://github.com/cakedc/users/tree/11.next-cake4) | 11.0.0 | stable | +| ^4.0 | [9.0](https://github.com/cakedc/users/tree/9.next) | 9.0.5 | stable | | ^3.7 <4.0 | [8.5](https://github.com/cakedc/users/tree/8.next) | 8.5.1 | stable | -| 3.7 | [develop](https://github.com/cakedc/users/tree/develop) | - | unstable | -| 3.6 | [8.1](https://github.com/cakedc/users/tree/8.1.0) | 8.1.0 | stable | -| 3.5 | [6.x](https://github.com/cakedc/users/tree/6.x) | 6.0.1 | stable | -| 3.4 | [5.x](https://github.com/cakedc/users/tree/5.x) | 5.2.0 | stable | -| 3.3 | [4.x](https://github.com/cakedc/users/tree/4.x) | 4.2.0 | stable | -| 2.x | [2.x](https://github.com/cakedc/users/tree/2.x) | 2.2.0 |stable | +| ^3.7 <4.0 | [develop](https://github.com/cakedc/users/tree/develop) | - | unstable | +| 3.6 | [8.1](https://github.com/cakedc/users/tree/8.1.0) | 8.1.0 | stable | +| 3.5 | [6.x](https://github.com/cakedc/users/tree/6.x) | 6.0.1 | stable | +| 3.4 | [5.x](https://github.com/cakedc/users/tree/5.x) | 5.2.0 | stable | +| >=3.2.9 <3.4.0 | [4.x](https://github.com/cakedc/users/tree/4.x) | 4.2.1 | stable | +| ^2.10 | [2.x](https://github.com/cakedc/users/tree/2.x) | 2.2.0 |stable | -The **Users** plugin is back! +The **Users** plugin covers the following features: -It covers the following features: * User registration * Login/logout * Social login (Facebook, Twitter, Instagram, Google, Linkedin, etc) @@ -36,6 +36,7 @@ It covers the following features: * One-Time Password for Two-Factor Authentication The plugin is here to provide users related features following 2 approaches: + * Quick drop-in working solution for users login/registration. Get users working in 5 minutes. * Extensible solution for a bigger/custom application. You'll be able to extend: * UsersAuth Component @@ -51,8 +52,8 @@ Another decision made was limiting the plugin dependencies on other packages as Requirements ------------ -* CakePHP 3.6.0+ -* PHP 5.6+ +* CakePHP 4.0+ +* PHP 7.2+ Documentation ------------- diff --git a/composer.json b/composer.json index 96e3b0b23..3a94372d7 100644 --- a/composer.json +++ b/composer.json @@ -27,15 +27,16 @@ "source": "https://github.com/CakeDC/users" }, "minimum-stability": "dev", + "prefer-stable": true, "require": { "php": ">=7.2.0", - "cakephp/cakephp": "^4.0.0", - "cakedc/auth": "^6.0.0", + "cakephp/cakephp": "^4.0", + "cakedc/auth": "^7.0", "cakephp/authorization": "^2.0.0", "cakephp/authentication": "^2.0.0" }, "require-dev": { - "phpunit/phpunit": "^8.0", + "phpunit/phpunit": "^9.5", "league/oauth2-facebook": "@stable", "league/oauth2-instagram": "@stable", "league/oauth2-google": "@stable", @@ -46,7 +47,8 @@ "yubico/u2flib-server": "^1.0", "php-coveralls/php-coveralls": "^2.1", "league/oauth1-client": "^1.7", - "cakephp/cakephp-codesniffer": "^4.0" + "cakephp/cakephp-codesniffer": "^4.0", + "web-auth/webauthn-lib": "v3.3.x-dev" }, "suggest": { "league/oauth1-client": "Provides Social Authentication with Twitter", @@ -86,11 +88,11 @@ "test": "phpunit --stderr", "stan": "phpstan analyse src/", "psalm": "php vendor/psalm/phar/psalm.phar --show-info=false src/ ", - "stan-setup": "cp composer.json composer.backup && composer require --dev phpstan/phpstan:^0.12.7 psalm/phar:~3.8.0 && mv composer.backup composer.json", + "stan-setup": "cp composer.json composer.backup && composer require --dev phpstan/phpstan:0.12.94 psalm/phar:~4.9.2 && mv composer.backup composer.json", "stan-rebuild-baseline": "phpstan analyse --configuration phpstan.neon --error-format baselineNeon src/ > phpstan-baseline.neon", "psalm-rebuild-baseline": "php vendor/psalm/phar/psalm.phar --show-info=false --set-baseline=psalm-baseline.xml src/", "rector": "rector process src/", - "rector-setup": "cp composer.json composer.backup && composer require --dev rector/rector:^0.4.11 && mv composer.backup composer.json", + "rector-setup": "cp composer.json composer.backup && composer require --dev rector/rector:^0.11.2 && mv composer.backup composer.json", "coverage-test": "phpunit --stderr --coverage-clover=clover.xml" } } diff --git a/config/Migrations/20210929202041_AddLastLoginToUsers.php b/config/Migrations/20210929202041_AddLastLoginToUsers.php new file mode 100644 index 000000000..5a533c250 --- /dev/null +++ b/config/Migrations/20210929202041_AddLastLoginToUsers.php @@ -0,0 +1,24 @@ +table('users'); + $table->addColumn('last_login', 'datetime', [ + 'default' => null, + 'null' => true, + ]); + $table->update(); + } +} diff --git a/config/Migrations/schema-dump-default.lock b/config/Migrations/schema-dump-default.lock new file mode 100644 index 000000000..9c437b3c1 Binary files /dev/null and b/config/Migrations/schema-dump-default.lock differ diff --git a/config/permissions.php b/config/permissions.php index abb75d2d0..5c52467fb 100644 --- a/config/permissions.php +++ b/config/permissions.php @@ -79,6 +79,11 @@ 'u2fRegisterFinish', 'u2fAuthenticate', 'u2fAuthenticateFinish', + 'webauthn2fa', + 'webauthn2faRegister', + 'webauthn2faRegisterOptions', + 'webauthn2faAuthenticate', + 'webauthn2faAuthenticateOptions', ], 'bypassAuth' => true, ], @@ -128,5 +133,12 @@ 'controller' => 'Pages', 'action' => 'display', ], + [ + 'role' => '*', + 'plugin' => 'DebugKit', + 'controller' => '*', + 'action' => '*', + 'bypassAuth' => true, + ], ] ]; diff --git a/config/routes.php b/config/routes.php index 023c99f7d..81ac70cc1 100644 --- a/config/routes.php +++ b/config/routes.php @@ -38,7 +38,7 @@ if (is_array($oauthPath)) { $routes->scope('/auth', function (RouteBuilder $routes) use ($oauthPath) { $routes->connect( - '/:provider', + '/{provider}', $oauthPath, ['provider' => implode('|', array_keys(Configure::read('OAuth.providers')))] ); @@ -49,4 +49,4 @@ 'plugin' => 'CakeDC/Users', 'controller' => 'SocialAccounts', ]); -$routes->connect('/users/:action/*', UsersUrl::actionRouteParams(null)); +$routes->connect('/users/{action}/*', UsersUrl::actionRouteParams(null)); diff --git a/config/users.php b/config/users.php index dc491b080..23dfeb9d1 100644 --- a/config/users.php +++ b/config/users.php @@ -10,7 +10,25 @@ * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ +use Cake\Core\Configure; +use Cake\Log\Log; use Cake\Routing\Router; +use Laminas\Diactoros\Uri; + +$allowedRedirectHosts = [ + 'localhost', +]; +if (Configure::read('App.fullBaseUrl')) { + try { + $uri = new Uri(Configure::read('App.fullBaseUrl')); + $fullBaseHost = $uri->getHost(); + if ($fullBaseHost) { + $allowedRedirectHosts[] = $fullBaseHost; + } + } catch (Exception $ex) { + Log::warning('Invalid host from App.fullBasedUrl in CakeDC/Users configuration: ' . $ex->getMessage()); + } +} $config = [ 'Users' => [ @@ -40,6 +58,8 @@ 'ensureActive' => false, // default role name used in registration 'defaultRole' => 'user', + // show verbose error to users + 'showVerboseError' => false, ], 'reCaptcha' => [ // reCaptcha key goes here @@ -72,7 +92,7 @@ ], // form key to store the social auth data 'Form' => [ - 'social' => 'social' + 'social' => 'social', ], 'Data' => [ // data key to store the users email @@ -92,12 +112,14 @@ 'Cookie' => [ 'name' => 'remember_me', 'Config' => [ - 'expires' => '1 month', - 'httpOnly' => true, - ] - ] + 'expires' => new \DateTime('+1 month'), + 'httponly' => true, + ], + ], ], 'Superuser' => ['allowedToChangePasswords' => false], // able to reset any users password + // list of valid hosts to allow redirects after valid login via the `redirect` query param + 'AllowedRedirectHosts' => $allowedRedirectHosts, ], 'OneTimePasswordAuthenticator' => [ 'checker' => \CakeDC\Auth\Authentication\DefaultOneTimePasswordAuthenticationChecker::class, @@ -112,21 +134,27 @@ // QR-code provider (more on this later) 'qrcodeprovider' => null, // Random Number Generator provider (more on this later) - 'rngprovider' => null + 'rngprovider' => null, ], 'U2f' => [ 'enabled' => false, 'checker' => \CakeDC\Auth\Authentication\DefaultU2fAuthenticationChecker::class, ], + 'Webauthn2fa' => [ + 'enabled' => false, + 'appName' => null,//App must set a valid name here + 'id' => null,//default value is the current domain + 'checker' => \CakeDC\Auth\Authentication\DefaultWebauthn2fAuthenticationChecker::class, + ], // default configuration used to auto-load the Auth Component, override to change the way Auth works 'Auth' => [ 'Authentication' => [ - 'serviceLoader' => \CakeDC\Users\Loader\AuthenticationServiceLoader::class + 'serviceLoader' => \CakeDC\Users\Loader\AuthenticationServiceLoader::class, ], 'AuthenticationComponent' => [ 'load' => true, 'loginRedirect' => '/', - 'requireIdentity' => false + 'requireIdentity' => false, ], 'Authenticators' => [ 'Session' => [ @@ -150,8 +178,8 @@ 'skipTwoFactorVerify' => true, 'rememberMeField' => 'remember_me', 'cookie' => [ - 'expires' => '1 month', - 'httpOnly' => true, + 'expires' => new \DateTime('+1 month'), + 'httponly' => true, ], 'urlChecker' => 'Authentication.CakeRouter', ], @@ -162,41 +190,41 @@ 'SocialPendingEmail' => [ 'className' => 'CakeDC/Users.SocialPendingEmail', 'skipTwoFactorVerify' => true, - ] + ], ], 'Identifiers' => [ 'Password' => [ 'className' => 'Authentication.Password', 'fields' => [ 'username' => ['username', 'email'], - 'password' => 'password' + 'password' => 'password', ], 'resolver' => [ 'className' => 'Authentication.Orm', - 'finder' => 'active' + 'finder' => 'active', ], ], - "Social" => [ + 'Social' => [ 'className' => 'CakeDC/Users.Social', - 'authFinder' => 'active' + 'authFinder' => 'active', ], 'Token' => [ 'className' => 'Authentication.Token', 'tokenField' => 'api_token', 'resolver' => [ 'className' => 'Authentication.Orm', - 'finder' => 'active' + 'finder' => 'active', ], - ] + ], ], - "Authorization" => [ + 'Authorization' => [ 'enable' => true, - 'serviceLoader' => \CakeDC\Users\Loader\AuthorizationServiceLoader::class + 'serviceLoader' => \CakeDC\Users\Loader\AuthorizationServiceLoader::class, ], 'AuthorizationMiddleware' => [ 'unauthorizedHandler' => [ 'className' => 'CakeDC/Users.DefaultRedirect', - ] + ], ], 'AuthorizationComponent' => [ 'enabled' => true, @@ -204,7 +232,7 @@ 'RbacPolicy' => [], 'PasswordRehash' => [ 'identifiers' => ['Password'], - ] + ], ], 'OAuth' => [ 'providers' => [ @@ -218,7 +246,7 @@ 'redirectUri' => Router::fullBaseUrl() . '/auth/facebook', 'linkSocialUri' => Router::fullBaseUrl() . '/link-social/facebook', 'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/facebook', - ] + ], ], 'twitter' => [ 'service' => 'CakeDC\Auth\Social\Service\OAuth1Service', @@ -228,7 +256,7 @@ 'redirectUri' => Router::fullBaseUrl() . '/auth/twitter', 'linkSocialUri' => Router::fullBaseUrl() . '/link-social/twitter', 'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/twitter', - ] + ], ], 'linkedIn' => [ 'service' => 'CakeDC\Auth\Social\Service\OAuth2Service', @@ -238,7 +266,7 @@ 'redirectUri' => Router::fullBaseUrl() . '/auth/linkedIn', 'linkSocialUri' => Router::fullBaseUrl() . '/link-social/linkedIn', 'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/linkedIn', - ] + ], ], 'instagram' => [ 'service' => 'CakeDC\Auth\Social\Service\OAuth2Service', @@ -248,7 +276,7 @@ 'redirectUri' => Router::fullBaseUrl() . '/auth/instagram', 'linkSocialUri' => Router::fullBaseUrl() . '/link-social/instagram', 'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/instagram', - ] + ], ], 'google' => [ 'service' => 'CakeDC\Auth\Social\Service\OAuth2Service', @@ -259,7 +287,7 @@ 'redirectUri' => Router::fullBaseUrl() . '/auth/google', 'linkSocialUri' => Router::fullBaseUrl() . '/link-social/google', 'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/google', - ] + ], ], 'amazon' => [ 'service' => 'CakeDC\Auth\Social\Service\OAuth2Service', @@ -269,7 +297,7 @@ 'redirectUri' => Router::fullBaseUrl() . '/auth/amazon', 'linkSocialUri' => Router::fullBaseUrl() . '/link-social/amazon', 'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/amazon', - ] + ], ], 'cognito' => [ 'service' => 'CakeDC\Auth\Social\Service\OAuth2Service', @@ -279,11 +307,11 @@ 'redirectUri' => Router::fullBaseUrl() . '/auth/cognito', 'linkSocialUri' => Router::fullBaseUrl() . '/link-social/cognito', 'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/cognito', - 'scope' => 'email openid' - ] + 'scope' => 'email openid', + ], ], ], - ] + ], ]; return $config; diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index c670af94d..52b966521 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,313 +1,267 @@ - - parameters: ignoreErrors: - message: "#^Access to an undefined property Cake\\\\Controller\\\\Controller\\:\\:\\$Authentication\\.$#" count: 1 - path: src\Controller\Component\LoginComponent.php + path: src/Controller/Component/LoginComponent.php - message: "#^Call to an undefined method Cake\\\\Controller\\\\Controller\\:\\:getUsersTable\\(\\)\\.$#" - count: 1 - path: src\Controller\Component\LoginComponent.php - - - - message: "#^Call to an undefined method CakeDC\\\\Users\\\\Model\\\\Table\\\\SocialAccountsTable\\:\\:validateAccount\\(\\)\\.$#" - count: 1 - path: src\Controller\SocialAccountsController.php - - - - message: "#^Call to an undefined method CakeDC\\\\Users\\\\Model\\\\Table\\\\SocialAccountsTable\\:\\:resendValidation\\(\\)\\.$#" - count: 1 - path: src\Controller\SocialAccountsController.php - - - - message: "#^Call to an undefined method Cake\\\\ORM\\\\Table\\:\\:linkSocialAccount\\(\\)\\.$#" - count: 1 - path: src\Controller\UsersController.php - - - - message: "#^Call to an undefined method Cake\\\\Controller\\\\Component\\:\\:handleLogin\\(\\)\\.$#" - count: 3 - path: src\Controller\UsersController.php + count: 2 + path: src/Controller/Component/LoginComponent.php - message: "#^Access to an undefined property CakeDC\\\\Users\\\\Controller\\\\UsersController\\:\\:\\$Authentication\\.$#" count: 2 - path: src\Controller\UsersController.php - - - - message: "#^Parameter \\#1 \\$user of method CakeDC\\\\Users\\\\Controller\\\\UsersController\\:\\:onVerifyGetSecret\\(\\) expects CakeDC\\\\Users\\\\Model\\\\Entity\\\\User, array\\|string\\|null given\\.$#" - count: 1 - path: src\Controller\UsersController.php + path: src/Controller/UsersController.php - message: "#^Access to an undefined property CakeDC\\\\Users\\\\Controller\\\\UsersController\\:\\:\\$OneTimePasswordAuthenticator\\.$#" count: 3 - path: src\Controller\UsersController.php + path: src/Controller/UsersController.php - - message: "#^Parameter \\#2 \\$user of method CakeDC\\\\Users\\\\Controller\\\\UsersController\\:\\:onPostVerifyCodeOkay\\(\\) expects CakeDC\\\\Users\\\\Model\\\\Entity\\\\User, array\\|string\\|null given\\.$#" + message: "#^Access to an undefined property Cake\\\\Datasource\\\\EntityInterface\\:\\:\\$u2f_registration\\.$#" count: 1 - path: src\Controller\UsersController.php + path: src/Controller/UsersController.php - - message: "#^Call to an undefined method Cake\\\\ORM\\\\Table\\:\\:validationPasswordConfirm\\(\\)\\.$#" - count: 1 - path: src\Controller\UsersController.php + message: "#^Call to an undefined method Cake\\\\Controller\\\\Component\\:\\:handleLogin\\(\\)\\.$#" + count: 3 + path: src/Controller/UsersController.php - - message: "#^Call to an undefined method Cake\\\\ORM\\\\Table\\:\\:validationCurrentPassword\\(\\)\\.$#" + message: "#^Call to an undefined method Cake\\\\ORM\\\\Table\\:\\:changePassword\\(\\)\\.$#" count: 1 - path: src\Controller\UsersController.php + path: src/Controller/UsersController.php - - message: "#^Call to an undefined method Cake\\\\ORM\\\\Table\\:\\:changePassword\\(\\)\\.$#" + message: "#^Call to an undefined method Cake\\\\ORM\\\\Table\\:\\:linkSocialAccount\\(\\)\\.$#" count: 1 - path: src\Controller\UsersController.php + path: src/Controller/UsersController.php + + - + message: "#^Call to an undefined method Cake\\\\ORM\\\\Table\\:\\:register\\(\\)\\.$#" + count: 2 + path: src/Controller/UsersController.php - message: "#^Call to an undefined method Cake\\\\ORM\\\\Table\\:\\:resetToken\\(\\)\\.$#" count: 2 - path: src\Controller\UsersController.php + path: src/Controller/UsersController.php - - message: "#^Parameter \\#2 \\$options of method Cake\\\\Controller\\\\Component\\\\FlashComponent\\:\\:success\\(\\) expects array, string given\\.$#" - count: 1 - path: src\Controller\UsersController.php + message: "#^Call to an undefined method Cake\\\\ORM\\\\Table\\:\\:validate\\(\\)\\.$#" + count: 2 + path: src/Controller/UsersController.php - - message: "#^Parameter \\#2 \\$options of method Cake\\\\Controller\\\\Component\\\\FlashComponent\\:\\:error\\(\\) expects array, string given\\.$#" + message: "#^Call to an undefined method Cake\\\\ORM\\\\Table\\:\\:validationCurrentPassword\\(\\)\\.$#" count: 1 - path: src\Controller\UsersController.php + path: src/Controller/UsersController.php - - message: "#^Method CakeDC\\\\Users\\\\Controller\\\\UsersController\\:\\:_getReCaptchaInstance\\(\\) should return ReCaptcha\\\\ReCaptcha but returns null\\.$#" + message: "#^Call to an undefined method Cake\\\\ORM\\\\Table\\:\\:validationPasswordConfirm\\(\\)\\.$#" count: 1 - path: src\Controller\UsersController.php + path: src/Controller/UsersController.php - - message: "#^Call to an undefined method Cake\\\\ORM\\\\Table\\:\\:register\\(\\)\\.$#" - count: 2 - path: src\Controller\UsersController.php + message: "#^Parameter \\#1 \\$message of method Cake\\\\Controller\\\\Controller\\:\\:log\\(\\) expects string, Exception given\\.$#" + count: 1 + path: src/Controller/UsersController.php - message: "#^Parameter \\#1 \\$object of method Cake\\\\Controller\\\\Controller\\:\\:paginate\\(\\) expects Cake\\\\ORM\\\\Query\\|Cake\\\\ORM\\\\Table\\|string\\|null, Cake\\\\Datasource\\\\RepositoryInterface given\\.$#" count: 1 - path: src\Controller\UsersController.php + path: src/Controller/UsersController.php - - message: "#^Access to an undefined property Cake\\\\Datasource\\\\EntityInterface\\:\\:\\$u2f_registration\\.$#" + message: "#^Parameter \\#2 \\$options of method Cake\\\\Controller\\\\Component\\\\FlashComponent\\:\\:error\\(\\) expects array, string given\\.$#" count: 1 - path: src\Controller\UsersController.php - - - - message: "#^Call to an undefined method Cake\\\\ORM\\\\Table\\:\\:validate\\(\\)\\.$#" - count: 2 - path: src\Controller\UsersController.php + path: src/Controller/UsersController.php - - message: "#^Method CakeDC\\\\Users\\\\Authenticator\\\\SocialPendingEmailAuthenticator\\:\\:_getReCaptchaInstance\\(\\) should return ReCaptcha\\\\ReCaptcha but returns null\\.$#" + message: "#^Parameter \\#2 \\$options of method Cake\\\\Controller\\\\Component\\\\FlashComponent\\:\\:success\\(\\) expects array, string given\\.$#" count: 1 - path: src\Authenticator\SocialPendingEmailAuthenticator.php + path: src/Controller/UsersController.php - - message: "#^Call to an undefined method Cake\\\\ORM\\\\Table\\:\\:socialLogin\\(\\)\\.$#" + message: "#^Parameter \\#2 \\$usersTable of class CakeDC\\\\Users\\\\Webauthn\\\\RegisterAdapter constructor expects CakeDC\\\\Users\\\\Model\\\\Table\\\\UsersTable\\|null, Cake\\\\ORM\\\\Table given\\.$#" count: 1 - path: src\Identifier\SocialIdentifier.php + path: src/Controller/UsersController.php - - message: "#^Parameter \\#2 \\$request of method CakeDC\\\\Users\\\\Utility\\\\UsersUrl\\:\\:checkActionOnRequest\\(\\) expects Cake\\\\Http\\\\ServerRequest, Psr\\\\Http\\\\Message\\\\ServerRequestInterface given\\.$#" + message: "#^Call to an undefined method Cake\\\\ORM\\\\Table\\:\\:socialLogin\\(\\)\\.$#" count: 1 - path: src\Middleware\SocialAuthMiddleware.php + path: src/Identifier/SocialIdentifier.php - message: "#^Parameter \\#1 \\$request of method CakeDC\\\\Users\\\\Middleware\\\\SocialAuthMiddleware\\:\\:goNext\\(\\) expects Cake\\\\Http\\\\ServerRequest, Psr\\\\Http\\\\Message\\\\ServerRequestInterface given\\.$#" count: 1 - path: src\Middleware\SocialAuthMiddleware.php + path: src/Middleware/SocialAuthMiddleware.php - message: "#^Parameter \\#2 \\$request of method CakeDC\\\\Users\\\\Utility\\\\UsersUrl\\:\\:checkActionOnRequest\\(\\) expects Cake\\\\Http\\\\ServerRequest, Psr\\\\Http\\\\Message\\\\ServerRequestInterface given\\.$#" count: 1 - path: src\Middleware\SocialEmailMiddleware.php + path: src/Middleware/SocialAuthMiddleware.php - message: "#^Parameter \\#1 \\$request of method CakeDC\\\\Users\\\\Middleware\\\\SocialAuthMiddleware\\:\\:goNext\\(\\) expects Cake\\\\Http\\\\ServerRequest, Psr\\\\Http\\\\Message\\\\ServerRequestInterface given\\.$#" count: 1 - path: src\Middleware\SocialEmailMiddleware.php + path: src/Middleware/SocialEmailMiddleware.php - - message: "#^Call to an undefined method Cake\\\\Datasource\\\\EntityInterface\\:\\:updateToken\\(\\)\\.$#" + message: "#^Parameter \\#2 \\$request of method CakeDC\\\\Users\\\\Utility\\\\UsersUrl\\:\\:checkActionOnRequest\\(\\) expects Cake\\\\Http\\\\ServerRequest, Psr\\\\Http\\\\Message\\\\ServerRequestInterface given\\.$#" count: 1 - path: src\Model\Behavior\BaseTokenBehavior.php + path: src/Middleware/SocialEmailMiddleware.php - - message: "#^Access to an undefined property Cake\\\\ORM\\\\Table\\:\\:\\$SocialAccounts\\.$#" - count: 5 - path: src\Model\Behavior\LinkSocialBehavior.php - - - - message: "#^Negated boolean expression is always false\\.$#" + message: "#^Call to an undefined method Cake\\\\Datasource\\\\EntityInterface\\:\\:updateToken\\(\\)\\.$#" count: 1 - path: src\Model\Behavior\LinkSocialBehavior.php + path: src/Model/Behavior/BaseTokenBehavior.php - message: "#^Access to an undefined property Cake\\\\Datasource\\\\EntityInterface\\:\\:\\$social_accounts\\.$#" count: 2 - path: src\Model\Behavior\LinkSocialBehavior.php + path: src/Model/Behavior/LinkSocialBehavior.php - - message: "#^Method CakeDC\\\\Users\\\\Model\\\\Behavior\\\\PasswordBehavior\\:\\:resetToken\\(\\) should return string but returns Cake\\\\Datasource\\\\EntityInterface\\|false\\.$#" - count: 1 - path: src\Model\Behavior\PasswordBehavior.php + message: "#^Access to an undefined property Cake\\\\ORM\\\\Table\\:\\:\\$SocialAccounts\\.$#" + count: 5 + path: src/Model/Behavior/LinkSocialBehavior.php - - message: "#^Call to an undefined method Cake\\\\ORM\\\\Table\\:\\:findByUsernameOrEmail\\(\\)\\.$#" + message: "#^Negated boolean expression is always false\\.$#" count: 1 - path: src\Model\Behavior\PasswordBehavior.php + path: src/Model/Behavior/LinkSocialBehavior.php - message: "#^Access to an undefined property Cake\\\\Datasource\\\\EntityInterface\\:\\:\\$password\\.$#" count: 1 - path: src\Model\Behavior\PasswordBehavior.php + path: src/Model/Behavior/PasswordBehavior.php - - message: "#^Call to an undefined method Cake\\\\Datasource\\\\EntityInterface\\:\\:checkPassword\\(\\)\\.$#" + message: "#^Access to an undefined property Cake\\\\Datasource\\\\EntityInterface\\:\\:\\$password_confirm\\.$#" count: 1 - path: src\Model\Behavior\PasswordBehavior.php + path: src/Model/Behavior/PasswordBehavior.php - - message: "#^Access to an undefined property Cake\\\\Datasource\\\\EntityInterface\\:\\:\\$password_confirm\\.$#" + message: "#^Call to an undefined method Cake\\\\Datasource\\\\EntityInterface\\:\\:checkPassword\\(\\)\\.$#" count: 1 - path: src\Model\Behavior\PasswordBehavior.php + path: src/Model/Behavior/PasswordBehavior.php - - message: "#^Access to an undefined property CakeDC\\\\Users\\\\Model\\\\Behavior\\\\RegisterBehavior\\:\\:\\$validateEmail\\.$#" - count: 4 - path: src\Model\Behavior\RegisterBehavior.php + message: "#^Call to an undefined method Cake\\\\ORM\\\\Table\\:\\:findByUsernameOrEmail\\(\\)\\.$#" + count: 1 + path: src/Model/Behavior/PasswordBehavior.php - - message: "#^Access to an undefined property CakeDC\\\\Users\\\\Model\\\\Behavior\\\\RegisterBehavior\\:\\:\\$useTos\\.$#" + message: "#^Method CakeDC\\\\Users\\\\Model\\\\Behavior\\\\PasswordBehavior\\:\\:resetToken\\(\\) should return string but returns Cake\\\\Datasource\\\\EntityInterface\\|false\\.$#" count: 1 - path: src\Model\Behavior\RegisterBehavior.php + path: src/Model/Behavior/PasswordBehavior.php - - message: "#^Access to an undefined property Cake\\\\Datasource\\\\EntityInterface\\:\\:\\$validated\\.$#" + message: "#^Access to an undefined property Cake\\\\Datasource\\\\EntityInterface\\:\\:\\$activation_date\\.$#" count: 1 - path: src\Model\Behavior\RegisterBehavior.php + path: src/Model/Behavior/RegisterBehavior.php - - message: "#^Access to an undefined property Cake\\\\ORM\\\\Table\\:\\:\\$isValidateEmail\\.$#" + message: "#^Access to an undefined property Cake\\\\Datasource\\\\EntityInterface\\:\\:\\$active\\.$#" count: 1 - path: src\Model\Behavior\RegisterBehavior.php + path: src/Model/Behavior/RegisterBehavior.php - - message: "#^Cannot call method tokenExpired\\(\\) on array\\|Cake\\\\Datasource\\\\EntityInterface\\.$#" + message: "#^Access to an undefined property Cake\\\\Datasource\\\\EntityInterface\\:\\:\\$token_expires\\.$#" count: 1 - path: src\Model\Behavior\RegisterBehavior.php + path: src/Model/Behavior/RegisterBehavior.php - - message: "#^Access to an undefined property Cake\\\\Datasource\\\\EntityInterface\\:\\:\\$active\\.$#" + message: "#^Access to an undefined property Cake\\\\Datasource\\\\EntityInterface\\:\\:\\$validated\\.$#" count: 1 - path: src\Model\Behavior\RegisterBehavior.php + path: src/Model/Behavior/RegisterBehavior.php - - message: "#^Access to an undefined property Cake\\\\Datasource\\\\EntityInterface\\:\\:\\$activation_date\\.$#" + message: "#^Access to an undefined property Cake\\\\ORM\\\\Table\\:\\:\\$isValidateEmail\\.$#" count: 1 - path: src\Model\Behavior\RegisterBehavior.php + path: src/Model/Behavior/RegisterBehavior.php - - message: "#^Access to an undefined property Cake\\\\Datasource\\\\EntityInterface\\:\\:\\$token_expires\\.$#" + message: "#^Call to an undefined method Cake\\\\ORM\\\\Table\\:\\:validationRegister\\(\\)\\.$#" count: 1 - path: src\Model\Behavior\RegisterBehavior.php + path: src/Model/Behavior/RegisterBehavior.php - - message: "#^Call to an undefined method Cake\\\\ORM\\\\Table\\:\\:validationRegister\\(\\)\\.$#" + message: "#^Cannot call method tokenExpired\\(\\) on array\\|Cake\\\\Datasource\\\\EntityInterface\\.$#" count: 1 - path: src\Model\Behavior\RegisterBehavior.php + path: src/Model/Behavior/RegisterBehavior.php - - message: "#^Cannot access property \\$token on array\\|Cake\\\\Datasource\\\\EntityInterface\\.$#" + message: "#^Access to an undefined property CakeDC\\\\Users\\\\Model\\\\Entity\\\\SocialAccount\\:\\:\\$active\\.$#" count: 1 - path: src\Model\Behavior\SocialAccountBehavior.php + path: src/Model/Behavior/SocialAccountBehavior.php - message: "#^Cannot access property \\$active on array\\|Cake\\\\Datasource\\\\EntityInterface\\.$#" count: 2 - path: src\Model\Behavior\SocialAccountBehavior.php + path: src/Model/Behavior/SocialAccountBehavior.php - - message: "#^Method CakeDC\\\\Users\\\\Model\\\\Behavior\\\\SocialAccountBehavior\\:\\:validateAccount\\(\\) should return CakeDC\\\\Users\\\\Model\\\\Entity\\\\User but returns Cake\\\\Datasource\\\\EntityInterface\\.$#" + message: "#^Cannot access property \\$token on array\\|Cake\\\\Datasource\\\\EntityInterface\\.$#" count: 1 - path: src\Model\Behavior\SocialAccountBehavior.php + path: src/Model/Behavior/SocialAccountBehavior.php - - message: "#^Parameter \\#1 \\$socialAccount of method CakeDC\\\\Users\\\\Model\\\\Behavior\\\\SocialAccountBehavior\\:\\:_activateAccount\\(\\) expects CakeDC\\\\Users\\\\Model\\\\Entity\\\\SocialAccount, array\\|Cake\\\\Datasource\\\\EntityInterface given\\.$#" + message: "#^Cannot access property \\$user on array\\|Cake\\\\Datasource\\\\EntityInterface\\.$#" count: 1 - path: src\Model\Behavior\SocialAccountBehavior.php + path: src/Model/Behavior/SocialAccountBehavior.php - - message: "#^Cannot access property \\$user on array\\|Cake\\\\Datasource\\\\EntityInterface\\.$#" + message: "#^Method CakeDC\\\\Users\\\\Model\\\\Behavior\\\\SocialAccountBehavior\\:\\:resendValidation\\(\\) should return CakeDC\\\\Users\\\\Model\\\\Entity\\\\User but returns array\\.$#" count: 1 - path: src\Model\Behavior\SocialAccountBehavior.php + path: src/Model/Behavior/SocialAccountBehavior.php - - message: "#^Method CakeDC\\\\Users\\\\Model\\\\Behavior\\\\SocialAccountBehavior\\:\\:resendValidation\\(\\) should return CakeDC\\\\Users\\\\Model\\\\Entity\\\\User but returns array\\.$#" + message: "#^Method CakeDC\\\\Users\\\\Model\\\\Behavior\\\\SocialAccountBehavior\\:\\:validateAccount\\(\\) should return CakeDC\\\\Users\\\\Model\\\\Entity\\\\User but returns Cake\\\\Datasource\\\\EntityInterface\\.$#" count: 1 - path: src\Model\Behavior\SocialAccountBehavior.php + path: src/Model/Behavior/SocialAccountBehavior.php - - message: "#^Access to an undefined property CakeDC\\\\Users\\\\Model\\\\Entity\\\\SocialAccount\\:\\:\\$active\\.$#" + message: "#^Parameter \\#1 \\$socialAccount of method CakeDC\\\\Users\\\\Model\\\\Behavior\\\\SocialAccountBehavior\\:\\:_activateAccount\\(\\) expects CakeDC\\\\Users\\\\Model\\\\Entity\\\\SocialAccount, array\\|Cake\\\\Datasource\\\\EntityInterface given\\.$#" count: 1 - path: src\Model\Behavior\SocialAccountBehavior.php + path: src/Model/Behavior/SocialAccountBehavior.php - message: "#^Access to an undefined property Cake\\\\ORM\\\\Table\\:\\:\\$SocialAccounts\\.$#" count: 4 - path: src\Model\Behavior\SocialBehavior.php + path: src/Model/Behavior/SocialBehavior.php - message: "#^Access to an undefined property Cake\\\\ORM\\\\Table\\:\\:\\$isValidateEmail\\.$#" count: 1 - path: src\Model\Behavior\SocialBehavior.php + path: src/Model/Behavior/SocialBehavior.php - message: "#^Parameter \\#3 \\$tokenExpiration of method CakeDC\\\\Users\\\\Model\\\\Behavior\\\\BaseTokenBehavior\\:\\:_updateActive\\(\\) expects int, string given\\.$#" count: 1 - path: src\Model\Behavior\SocialBehavior.php + path: src/Model/Behavior/SocialBehavior.php - message: "#^Method CakeDC\\\\Users\\\\Model\\\\Entity\\\\User\\:\\:_setTos\\(\\) should return bool but returns string\\.$#" count: 1 - path: src\Model\Entity\User.php - - - - message: "#^Property CakeDC\\\\Users\\\\Shell\\\\UsersShell\\:\\:\\$Users \\(CakeDC\\\\Users\\\\Model\\\\Table\\\\UsersTable\\) does not accept Cake\\\\Datasource\\\\RepositoryInterface\\.$#" - count: 1 - path: src\Shell\UsersShell.php + path: src/Model/Entity/User.php - message: "#^Cannot access property \\$username on bool\\.$#" count: 2 - path: src\Shell\UsersShell.php - - - - message: "#^Call to an undefined method CakeDC\\\\Users\\\\Model\\\\Table\\\\UsersTable\\:\\:resetToken\\(\\)\\.$#" - count: 1 - path: src\Shell\UsersShell.php + path: src/Shell/UsersShell.php - - message: "#^Call to an undefined method CakeDC\\\\Users\\\\Model\\\\Table\\\\UsersTable\\:\\:generateUniqueUsername\\(\\)\\.$#" + message: "#^Property CakeDC\\\\Users\\\\Shell\\\\UsersShell\\:\\:\\$Users \\(CakeDC\\\\Users\\\\Model\\\\Table\\\\UsersTable\\) does not accept Cake\\\\Datasource\\\\RepositoryInterface\\.$#" count: 1 - path: src\Shell\UsersShell.php + path: src/Shell/UsersShell.php - - message: "#^Parameter \\#1 \\$message of method Cake\\\\Controller\\\\Controller\\:\\:log\\(\\)\\ expects string, Exception given.$#" + message: "#^Parameter \\#2 \\$usersTable of class CakeDC\\\\Users\\\\Webauthn\\\\Repository\\\\UserCredentialSourceRepository constructor expects CakeDC\\\\Users\\\\Model\\\\Table\\\\UsersTable\\|null, Cake\\\\ORM\\\\Table given\\.$#" count: 1 - path: src\Controller\Traits\OneTimePasswordVerifyTrait.php + path: src/Webauthn/BaseAdapter.php - - - message: "#^Access to an undefined property CakeDC\\\\Users\\\\View\\\\Helper\\\\AuthLinkHelper\\:\\:\\$Form\\.$#" - count: 1 - path: src\View\Helper\AuthLinkHelper.php diff --git a/phpstan.neon b/phpstan.neon index 0254599fe..f6dbf5f0b 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -5,8 +5,12 @@ parameters: level: 6 checkMissingIterableValueType: false checkGenericClassInNonGenericObjectType: false - autoload_files: + bootstrapFiles: - tests/bootstrap.php + excludes_analyse: + - src/Controller/SocialAccountsController.php + - src/Controller/UsersAccountsController.php + - src/Controller/AppAccountsController.php ignoreErrors: services: diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 180e56be1..3a1684f67 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -8,38 +8,26 @@ ~ @copyright Copyright 2010 - 2018, Cake Development Corporation (https://www.cakedc.com) ~ @license MIT License (http://www.opensource.org/licenses/mit-license.php) --> + + + + ./src + + + + + + + + + + + ./tests/TestCase + + + + + + - - - - - - - - - - ./tests/TestCase - - - - - ./src - - - - - - - - - - - diff --git a/rector.yml b/rector.yml deleted file mode 100644 index eaa99e6a6..000000000 --- a/rector.yml +++ /dev/null @@ -1,4 +0,0 @@ -# rector.yaml -services: - Rector\Php\Rector\FunctionLike\ParamTypeDeclarationRector: ~ - Rector\Php\Rector\FunctionLike\ReturnTypeDeclarationRector: ~ diff --git a/resources/locales/de_DE/users.mo b/resources/locales/de_DE/users.mo new file mode 100644 index 000000000..8408ca51f Binary files /dev/null and b/resources/locales/de_DE/users.mo differ diff --git a/resources/locales/de_DE/users.po b/resources/locales/de_DE/users.po new file mode 100644 index 000000000..7e9a16bce --- /dev/null +++ b/resources/locales/de_DE/users.po @@ -0,0 +1,941 @@ +# LANGUAGE translation of CakePHP Application +# Copyright YEAR NAME +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"POT-Creation-Date: 2022-03-11 13:46+0100\n" +"PO-Revision-Date: 2022-03-11 14:19+0100\n" +"Last-Translator: \n" +"Language-Team: LANGUAGE \n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.0.1\n" + +#: src/Controller/SocialAccountsController.php:50 +msgid "Account validated successfully" +msgstr "Das Konto wurde erfolgreich bestätigt" + +#: src/Controller/SocialAccountsController.php:52 +msgid "Account could not be validated" +msgstr "Das Konto konnte nicht bestätigt werden" + +#: src/Controller/SocialAccountsController.php:55 +msgid "Invalid token and/or social account" +msgstr "Ungültiger Token und/oder Social Account" + +#: src/Controller/SocialAccountsController.php:57 +#: src/Controller/SocialAccountsController.php:85 +msgid "Social Account already active" +msgstr "Social Account ist bereits aktiviert" + +#: src/Controller/SocialAccountsController.php:59 +msgid "Social Account could not be validated" +msgstr "Social Account konnte nicht bestätigt werden" + +#: src/Controller/SocialAccountsController.php:78 +msgid "Email sent successfully" +msgstr "Email wurde erfolgreich versendet" + +#: src/Controller/SocialAccountsController.php:80 +msgid "Email could not be sent" +msgstr "Email konnte nicht versendet werden" + +#: src/Controller/SocialAccountsController.php:83 +msgid "Invalid account" +msgstr "Ungültiges Konto" + +#: src/Controller/SocialAccountsController.php:87 +msgid "Email could not be resent" +msgstr "Email konnte nicht versendet werden" + +#: src/Controller/Traits/LinkSocialTrait.php:56 +msgid "Could not associate account, please try again." +msgstr "" +"Wir konnten das Konto leider nicht verbinden, bitte versuchen Sie es erneut." + +#: src/Controller/Traits/LinkSocialTrait.php:80 +msgid "Social account was associated." +msgstr "Social Account wurde verbunden." + +#: src/Controller/Traits/LoginTrait.php:73 +msgid "You've successfully logged out" +msgstr "Sie wurden erfolgreich abgemeldet" + +#: src/Controller/Traits/OneTimePasswordVerifyTrait.php:75 +msgid "Please enable Google Authenticator first." +msgstr "Bitte aktivieren Sie zuerst den Google Authenticator." + +#: src/Controller/Traits/OneTimePasswordVerifyTrait.php:90 +msgid "Could not find user data" +msgstr "Es konnten keine Benutzer-Daten gefunden werden" + +#: src/Controller/Traits/OneTimePasswordVerifyTrait.php:129 +msgid "Could not verify, please try again" +msgstr "Überprüfung ist fehlgeschlagen, bitte versuchen Sie es erneut" + +#: src/Controller/Traits/OneTimePasswordVerifyTrait.php:161 +msgid "Verification code is invalid. Try again" +msgstr "Überprüfungs-Code ist ungültig. Bitte versuchen Sie erneut" + +#: src/Controller/Traits/PasswordManagementTrait.php:63 +msgid "Changing another user's password is not allowed" +msgstr "Sie dürfen nicht das Passwort von einem anderen Benutzer ändern" + +#: src/Controller/Traits/PasswordManagementTrait.php:77 +#: src/Controller/Traits/PasswordManagementTrait.php:121 +#: src/Controller/Traits/ProfileTrait.php:54 +msgid "User was not found" +msgstr "Der Benutzer konnte nicht gefunden werden" + +#: src/Controller/Traits/PasswordManagementTrait.php:105 +#: src/Controller/Traits/PasswordManagementTrait.php:117 +#: src/Controller/Traits/PasswordManagementTrait.php:125 +msgid "Password could not be changed" +msgstr "Das Passwort konnte nicht geändert werden" + +#: src/Controller/Traits/PasswordManagementTrait.php:113 +msgid "Password has been changed successfully" +msgstr "Das Passwort wurde erfolgreich geändert" + +#: src/Controller/Traits/PasswordManagementTrait.php:167 +msgid "Please check your email to continue with password reset process" +msgstr "" +"Sie erhalten in Kürze eine Email mit Anweisungen wie Sie das Passwort " +"zurücksetzen können" + +#: src/Controller/Traits/PasswordManagementTrait.php:170 +#: src/Shell/UsersShell.php:286 +msgid "The password token could not be generated. Please try again" +msgstr "" +"Der Token für das Zurücksetzen des Passworts konnte nicht generiert werden. " +"Bitte versuchen Sie es erneut" + +#: src/Controller/Traits/PasswordManagementTrait.php:176 +#: src/Controller/Traits/UserValidationTrait.php:124 +msgid "User {0} was not found" +msgstr "Benutzer {0} konnte nicht gefunden werden" + +#: src/Controller/Traits/PasswordManagementTrait.php:178 +msgid "The user is not active" +msgstr "Der Benutzer ist inaktiv" + +#: src/Controller/Traits/PasswordManagementTrait.php:180 +#: src/Controller/Traits/UserValidationTrait.php:119 +#: src/Controller/Traits/UserValidationTrait.php:128 +msgid "Token could not be reset" +msgstr "Der Token konnte nicht zurückgesetzt werden" + +#: src/Controller/Traits/PasswordManagementTrait.php:204 +msgid "Google Authenticator token was successfully reset" +msgstr "Der Google Authenticator Token wurde erfolgreich zurückgesetzt" + +#: src/Controller/Traits/PasswordManagementTrait.php:207 +msgid "Could not reset Google Authenticator" +msgstr "Google Authenticator Token konnte nicht zurückgesetzt werden" + +#: src/Controller/Traits/ProfileTrait.php:58 +msgid "Not authorized, please login first" +msgstr "" +"Sie sind nicht berechtigt diese Seite aufzurufen. Bitte melden Sie sich " +"zuerst an" + +#: src/Controller/Traits/RegisterTrait.php:47 +msgid "You must log out to register a new user account" +msgstr "Sie müssen sich ausloggen um ein neues Konto zu erstellen" + +#: src/Controller/Traits/RegisterTrait.php:86 +#: src/Controller/Traits/RegisterTrait.php:117 +msgid "The user could not be saved" +msgstr "Der Benutzer konnte nicht gespeichert werden" + +#: src/Controller/Traits/RegisterTrait.php:103 +#: src/Loader/LoginComponentLoader.php:33 +msgid "Invalid reCaptcha" +msgstr "Ungültiger reCaptcha" + +#: src/Controller/Traits/RegisterTrait.php:151 +msgid "You have registered successfully, please log in" +msgstr "Sie haben sich erfolgreich registriert. Bitte melden Sie sich an" + +#: src/Controller/Traits/RegisterTrait.php:153 +msgid "Please validate your account before log in" +msgstr "Bitte bestätigen Sie ihr Konto bevor Sie sich anmelden" + +#: src/Controller/Traits/SimpleCrudTrait.php:77 +#: src/Controller/Traits/SimpleCrudTrait.php:107 +msgid "The {0} has been saved" +msgstr "Der {0} wurde gespeichert" + +#: src/Controller/Traits/SimpleCrudTrait.php:81 +#: src/Controller/Traits/SimpleCrudTrait.php:111 +msgid "The {0} could not be saved" +msgstr "Der {0} konnte nicht gespeichert werden" + +#: src/Controller/Traits/SimpleCrudTrait.php:131 +msgid "The {0} has been deleted" +msgstr "Der {0} wurde gelöscht" + +#: src/Controller/Traits/SimpleCrudTrait.php:133 +msgid "The {0} could not be deleted" +msgstr "Der {0} konnte nicht gelöscht werden" + +#: src/Controller/Traits/U2fTrait.php:216 +msgid "U2F requires SSL." +msgstr "U2F setzt SSL voraus." + +#: src/Controller/Traits/UserValidationTrait.php:49 +msgid "User account validated successfully" +msgstr "Sie haben Ihren Benutzer erfolgreich bestätigt" + +#: src/Controller/Traits/UserValidationTrait.php:51 +msgid "User account could not be validated" +msgstr "Ihr Benutzer konnte nicht bestätigt werden" + +#: src/Controller/Traits/UserValidationTrait.php:54 +msgid "User already active" +msgstr "Dieser Benutzer ist bereits aktiv" + +#: src/Controller/Traits/UserValidationTrait.php:60 +msgid "Reset password token was validated successfully" +msgstr "Der angegebenen Token für das Zurücksetzen des Passworts ist gültig" + +#: src/Controller/Traits/UserValidationTrait.php:68 +msgid "Reset password token could not be validated" +msgstr "Der angegebenen Token für das Zurücksetzen des Passworts ist unültig" + +#: src/Controller/Traits/UserValidationTrait.php:72 +msgid "Invalid validation type" +msgstr "Ungültiger Validierungstyp" + +#: src/Controller/Traits/UserValidationTrait.php:75 +msgid "Invalid token or user account already validated" +msgstr "Ungültiger Token oder Benutzer ist bereits validiert" + +#: src/Controller/Traits/UserValidationTrait.php:81 +msgid "Token already expired" +msgstr "Token ist bereits abgelaufen" + +#: src/Controller/Traits/UserValidationTrait.php:114 +msgid "Token has been reset successfully. Please check your email." +msgstr "" +"Der Token wurde erfolgreich zurückgesetzt. Bitte überprüfen Sie ihren " +"Posteingang." + +#: src/Controller/Traits/UserValidationTrait.php:126 +msgid "User {0} is already active" +msgstr "Benutzer {0} ist bereits aktiv" + +#: src/Controller/Traits/Webauthn2faTrait.php:53 +#: src/Controller/Traits/Webauthn2faTrait.php:73 +msgid "User already has configured webauthn2fa" +msgstr "Dieser Benutzer hat bereis Webauthn2fa aktiv" + +#: src/Controller/Traits/Webauthn2faTrait.php:77 +#: src/Controller/Traits/Webauthn2faTrait.php:122 +msgid "Register error with webauthn for user id: {0}" +msgstr "" +"Es trat ein Fehler bei der Registrierung des Webauthn2fa für die Benutzer " +"ID: {0} auf" + +#: src/Loader/AuthenticationServiceLoader.php:109 +msgid "Property {0}.className should be defined" +msgstr "Property {0}.className sollte definiert sein" + +#: src/Loader/LoginComponentLoader.php:31 +msgid "Username or password is incorrect" +msgstr "Benutzername oder Passwort ist nicht korrekt" + +#: src/Loader/LoginComponentLoader.php:52 +msgid "Could not proceed with social account. Please try again" +msgstr "" +"Es konnte keine Verbindung zum Social Account hergestellt werden. Bitte " +"versuchen Sie es erneut" + +#: src/Loader/LoginComponentLoader.php:54 +msgid "" +"Your user has not been validated yet. Please check your inbox for " +"instructions" +msgstr "" +"Ihr Benutzer wurde noch nicht bestätigt. Bitte überprüfen Sie ihren " +"Posteingang für weitere Anweisungen" + +#: src/Loader/LoginComponentLoader.php:58 +msgid "" +"Your social account has not been validated yet. Please check your inbox for " +"instructions" +msgstr "" +"Ihr Social Account wurde noch nicht bestätigt. Bitte überprüfen Sie ihren " +"Posteingang für weitere Anweisungen" + +#: src/Mailer/UsersMailer.php:36 +msgid "Your account validation link" +msgstr "Ihr Konto Validierungs-Link" + +#: src/Mailer/UsersMailer.php:63 +msgid "{0}Your reset password link" +msgstr "{0}Ihr Passwort Zurücksetzten Link" + +#: src/Mailer/UsersMailer.php:95 +msgid "{0}Your social account validation link" +msgstr "{0}Ihr Social Account Bestätigung Link" + +#: src/Middleware/SocialAuthMiddleware.php:47 +#: templates/Users/social_email.php:16 +msgid "Please enter your email" +msgstr "Bitte geben Sie ihre E-Mail Adresse ein" + +#: src/Middleware/SocialAuthMiddleware.php:57 +msgid "Could not identify your account, please try again" +msgstr "" +"Wir konnten Ihr Konto nicht identifizieren. Bitte versuchen Sie es erneut" + +#: src/Middleware/UnauthorizedHandler/DefaultRedirectHandler.php:112 +msgid "You are not authorized to access that location." +msgstr "Sie sind nicht berechtigt diese Seite aufzurufen." + +#: src/Model/Behavior/AuthFinderBehavior.php:49 +msgid "Missing 'username' in options data" +msgstr "Fehlender Parameter ‘username’ in angegeben Optionen" + +#: src/Model/Behavior/LinkSocialBehavior.php:52 +msgid "Social account already associated to another user" +msgstr "Dieser Social Account ist bereits mit einem anderen Benutzer verknüpft" + +#: src/Model/Behavior/PasswordBehavior.php:45 +msgid "Reference cannot be null" +msgstr "Referenz darf nicht leer sein" + +#: src/Model/Behavior/PasswordBehavior.php:50 +msgid "Token expiration cannot be empty" +msgstr "Token Ablaufdatum darf nicht leer sein" + +#: src/Model/Behavior/PasswordBehavior.php:56 +#: src/Model/Behavior/PasswordBehavior.php:136 +msgid "User not found" +msgstr "Benutzer konnte nicht gefunden werden" + +#: src/Model/Behavior/PasswordBehavior.php:60 +#: src/Model/Behavior/RegisterBehavior.php:129 +msgid "User account already validated" +msgstr "Benutzer ist bereits bestätigt" + +#: src/Model/Behavior/PasswordBehavior.php:66 +msgid "User not active" +msgstr "Benutzer ist nicht aktiv" + +#: src/Model/Behavior/PasswordBehavior.php:141 +msgid "The current password does not match" +msgstr "Das aktuelle Passwort stimmt nicht überein" + +#: src/Model/Behavior/PasswordBehavior.php:144 +msgid "You cannot use the current password as the new one" +msgstr "Sie dürfen nicht das aktuelle Passwort als neues Passwort verwenden" + +#: src/Model/Behavior/RegisterBehavior.php:107 +msgid "User not found for the given token and email." +msgstr "" +"Es konnte kein Benutzer mit dem angegebenen Token oder der angegebenen Email " +"gefunden werden." + +#: src/Model/Behavior/RegisterBehavior.php:110 +msgid "Token has already expired user with no token" +msgstr "Der Token ist bereits abgelaufen oder der Benutzer hat keinen Token" + +#: src/Model/Behavior/RegisterBehavior.php:167 +msgid "This field is required" +msgstr "Dieses Feld muss ausgefüllt werden" + +#: src/Model/Behavior/SocialAccountBehavior.php:100 +#: src/Model/Behavior/SocialAccountBehavior.php:130 +msgid "Account already validated" +msgstr "Konto ist bereits bestätigt" + +#: src/Model/Behavior/SocialAccountBehavior.php:104 +#: src/Model/Behavior/SocialAccountBehavior.php:135 +msgid "Account not found for the given token and email." +msgstr "" +"Es konnte kein Konto für den angegebenen Token oder die angegebene Email " +"gefunden werden." + +#: src/Model/Behavior/SocialBehavior.php:85 +msgid "Unable to login user with reference {0}" +msgstr "Der Login über die Referenz {0} konnte nicht durchgeführt werden" + +#: src/Model/Behavior/SocialBehavior.php:136 +msgid "Email not present" +msgstr "Email nicht vorhanden" + +#: src/Model/Table/UsersTable.php:107 +msgid "Your password does not match your confirm password. Please try again" +msgstr "Bitte bestätigen Sie ihr Passwort und versuchen Sie es erneut" + +#: src/Model/Table/UsersTable.php:201 +msgid "Username already exists" +msgstr "Benutzername ist bereits vorhanden" + +#: src/Model/Table/UsersTable.php:207 +msgid "Email already exists" +msgstr "Email ist bereits vorhanden" + +#: src/Shell/UsersShell.php:46 +msgid "Utilities for CakeDC Users Plugin" +msgstr "Tools für das CakeDC Users Plugin" + +#: src/Shell/UsersShell.php:48 +msgid "Activate an specific user" +msgstr "Aktiviere einen Benutzer" + +#: src/Shell/UsersShell.php:51 +msgid "Add a new superadmin user for testing purposes" +msgstr "Füge einen neuen Superadmin Benutzer für Testzwecke hinzu" + +#: src/Shell/UsersShell.php:54 +msgid "Add a new user" +msgstr "Füge einen neuen Benutzer hinzu" + +#: src/Shell/UsersShell.php:57 +msgid "Change the role for an specific user" +msgstr "Ändere die Rolle für einen Benutzer" + +#: src/Shell/UsersShell.php:60 +msgid "Change the api token for an specific user" +msgstr "Ändere den API Token für einen Benutzer" + +#: src/Shell/UsersShell.php:63 +msgid "Deactivate an specific user" +msgstr "Deaktiviere einen Benutzer" + +#: src/Shell/UsersShell.php:66 +msgid "Delete an specific user" +msgstr "Lösche einen Benutzer" + +#: src/Shell/UsersShell.php:69 +msgid "Reset the password via email" +msgstr "Sende Passwort-Zurücksetzen E-Mail für eine E-Mail Adresse" + +#: src/Shell/UsersShell.php:72 +msgid "Reset the password for all users" +msgstr "Setze das Passwort für alle Benutzer" + +#: src/Shell/UsersShell.php:75 +msgid "Reset the password for an specific user" +msgstr "Setze das Passwort für einen Benutzer" + +#: src/Shell/UsersShell.php:135 src/Shell/UsersShell.php:161 +msgid "Please enter a password." +msgstr "Bitte geben Sie das Passwort ein." + +#: src/Shell/UsersShell.php:139 +msgid "Password changed for all users" +msgstr "Das Passwort wurde für alle Benutzer geändert" + +#: src/Shell/UsersShell.php:140 src/Shell/UsersShell.php:168 +msgid "New password: {0}" +msgstr "Neues Passwort: {0}" + +#: src/Shell/UsersShell.php:158 src/Shell/UsersShell.php:186 +#: src/Shell/UsersShell.php:214 src/Shell/UsersShell.php:304 +#: src/Shell/UsersShell.php:403 +msgid "Please enter a username." +msgstr "Bitte geben Sie einen Benutzernamen ein." + +#: src/Shell/UsersShell.php:167 +msgid "Password changed for user: {0}" +msgstr "Das Passwort wurde für den Benutzer: {0} geändert" + +#: src/Shell/UsersShell.php:189 +msgid "Please enter a role." +msgstr "Bitte geben Sie eine Rolle ein." + +#: src/Shell/UsersShell.php:195 +msgid "Role changed for user: {0}" +msgstr "Die Rolle wurde für den Benutzer: {0} geändert" + +#: src/Shell/UsersShell.php:196 +msgid "New role: {0}" +msgstr "Neue Rolle: {0}" + +#: src/Shell/UsersShell.php:217 +msgid "Please enter a token." +msgstr "Bitte geben Sie einen Token ein." + +#: src/Shell/UsersShell.php:224 +msgid "User was not saved, check validation errors" +msgstr "" +"Der Benutzer konnte nicht gespeichert werden. Bitte überprüfe die " +"Validierungsfehlermeldungen" + +#: src/Shell/UsersShell.php:229 +msgid "Api token changed for user: {0}" +msgstr "API Token für Benutzer: {0} wurde geändert" + +#: src/Shell/UsersShell.php:230 +msgid "New token: {0}" +msgstr "Neuer Token: {0}" + +#: src/Shell/UsersShell.php:245 +msgid "User was activated: {0}" +msgstr "Benutzer wurde aktiviert: {0}" + +#: src/Shell/UsersShell.php:260 +msgid "User was de-activated: {0}" +msgstr "Benutzer wurde deaktiviert: {0}" + +#: src/Shell/UsersShell.php:272 +msgid "Please enter a username or email." +msgstr "Bitte geben Sie einen Benutzername oder eine Email ein." + +#: src/Shell/UsersShell.php:280 +msgid "" +"Please ask the user to check the email to continue with password reset " +"process" +msgstr "" +"Bitte Fragen Sie den Benutzer den Posteingang zu überprüfen für die weitere " +"Vorgehensweise" + +#: src/Shell/UsersShell.php:350 +msgid "Superuser added:" +msgstr "Superuser hinzugefügt:" + +#: src/Shell/UsersShell.php:352 +msgid "User added:" +msgstr "Benutzer hinzugefügt:" + +#: src/Shell/UsersShell.php:354 +msgid "Id: {0}" +msgstr "ID: {0}" + +#: src/Shell/UsersShell.php:355 +msgid "Username: {0}" +msgstr "Benutzername: {0}" + +#: src/Shell/UsersShell.php:356 +msgid "Email: {0}" +msgstr "Email: {0}" + +#: src/Shell/UsersShell.php:357 +msgid "Role: {0}" +msgstr "Rolle: {0}" + +#: src/Shell/UsersShell.php:358 +msgid "Password: {0}" +msgstr "Passwort: {0}" + +#: src/Shell/UsersShell.php:360 +msgid "User could not be added:" +msgstr "Benutzer konnte nicht hinzugefügt werden:" + +#: src/Shell/UsersShell.php:363 +msgid "Field: {0} Error: {1}" +msgstr "Feld: {0} Fehler: {0}" + +#: src/Shell/UsersShell.php:379 +msgid "The user was not found." +msgstr "Der Benutzer konnte nicht gefunden werden." + +#: src/Shell/UsersShell.php:414 +msgid "The user {0} was not deleted. Please try again" +msgstr "" +"Der Benutzer {0} konnte nicht gelöscht werden. Bitte versuchen Sie es erneut" + +#: src/Shell/UsersShell.php:416 +msgid "The user {0} was deleted successfully" +msgstr "Der Benutzer {0} wurde gelöscht" + +#: src/View/Helper/UserHelper.php:50 +msgid "Sign in with" +msgstr "Anmelden mit" + +#: src/View/Helper/UserHelper.php:113 +msgid "Logout" +msgstr "Abmelden" + +#: src/View/Helper/UserHelper.php:134 +msgid "Welcome, {0}" +msgstr "Willkommen, {0}" + +#: src/View/Helper/UserHelper.php:165 +msgid "reCaptcha is not configured! Please configure Users.reCaptcha.key" +msgstr "" +"reCaptcha ist nicht konfiguriert! Bitte setzten Sie Users.reCaptcha.key" + +#: src/View/Helper/UserHelper.php:218 +msgid "Connected with {0}" +msgstr "Mit {0} verbunden" + +#: src/View/Helper/UserHelper.php:223 +msgid "Connect with {0}" +msgstr "Mit {0} verbinden" + +#: templates/Users/add.php:13 templates/Users/edit.php:17 +#: templates/Users/index.php:13 templates/Users/index.php:26 +#: templates/Users/view.php:15 +msgid "Actions" +msgstr "Aktionen" + +#: templates/Users/add.php:15 templates/Users/edit.php:28 +#: templates/Users/view.php:23 +msgid "List Users" +msgstr "Benutzer auflisten" + +#: templates/Users/add.php:21 templates/Users/register.php:18 +msgid "Add User" +msgstr "Benutzer hinzufügen" + +#: templates/Users/add.php:23 templates/Users/edit.php:36 +#: templates/Users/index.php:22 templates/Users/login.php:20 +#: templates/Users/profile.php:30 templates/Users/register.php:20 +#: templates/Users/view.php:33 +msgid "Username" +msgstr "Benutzername" + +#: templates/Users/add.php:24 templates/Users/edit.php:37 +#: templates/Users/index.php:23 templates/Users/profile.php:32 +#: templates/Users/register.php:21 templates/Users/view.php:35 +msgid "Email" +msgstr "Email" + +#: templates/Users/add.php:25 templates/Users/login.php:21 +#: templates/Users/register.php:22 +msgid "Password" +msgstr "Passwort" + +#: templates/Users/add.php:26 templates/Users/edit.php:38 +#: templates/Users/index.php:24 templates/Users/register.php:28 +msgid "First name" +msgstr "Vorname" + +#: templates/Users/add.php:27 templates/Users/edit.php:39 +#: templates/Users/index.php:25 templates/Users/register.php:29 +msgid "Last name" +msgstr "Nachname" + +#: templates/Users/add.php:30 templates/Users/edit.php:54 +#: templates/Users/view.php:49 templates/Users/view.php:74 +msgid "Active" +msgstr "Aktiv" + +#: templates/Users/add.php:34 templates/Users/change_password.php:25 +#: templates/Users/edit.php:58 templates/Users/register.php:38 +#: templates/Users/request_reset_password.php:23 +#: templates/Users/resend_token_validation.php:20 +#: templates/Users/social_email.php:19 +msgid "Submit" +msgstr "Absenden" + +#: templates/Users/change_password.php:5 +msgid "Please enter the new password" +msgstr "Bitte geben Sie ihr neues Passwort ein" + +#: templates/Users/change_password.php:10 +msgid "Current password" +msgstr "Aktuelles Passwort" + +#: templates/Users/change_password.php:16 +msgid "New password" +msgstr "Neues Passwort" + +#: templates/Users/change_password.php:21 templates/Users/register.php:26 +msgid "Confirm password" +msgstr "Neues Passwort bestätigen" + +#: templates/Users/edit.php:22 templates/Users/index.php:40 +msgid "Delete" +msgstr "Löschen" + +#: templates/Users/edit.php:24 templates/Users/index.php:40 +#: templates/Users/view.php:21 +msgid "Are you sure you want to delete # {0}?" +msgstr "Sind Sie sicher, dass Sie # {0} löschen wollen?" + +#: templates/Users/edit.php:34 templates/Users/view.php:17 +msgid "Edit User" +msgstr "Benutzer bearbeiten" + +#: templates/Users/edit.php:40 templates/Users/view.php:43 +msgid "Token" +msgstr "Token" + +#: templates/Users/edit.php:42 +msgid "Token expires" +msgstr "Token Ablaufdatum" + +#: templates/Users/edit.php:45 +msgid "API token" +msgstr "API Token" + +#: templates/Users/edit.php:48 +msgid "Activation date" +msgstr "Aktivierungsdatum" + +#: templates/Users/edit.php:51 +msgid "TOS date" +msgstr "AGB Datum" + +#: templates/Users/edit.php:64 +msgid "Reset Google Authenticator Token" +msgstr "Google Authenticator Token zurücksetzen" + +#: templates/Users/edit.php:70 +msgid "Are you sure you want to reset token for user \"{0}\"?" +msgstr "" +"Sind Sie sicher, dass Sie den Token für den Benutzer # {0} zurücksetzen " +"wollen?" + +#: templates/Users/index.php:15 +msgid "New {0}" +msgstr "Neuer {0}" + +#: templates/Users/index.php:37 +msgid "View" +msgstr "Ansicht" + +#: templates/Users/index.php:38 +msgid "Change password" +msgstr "Passwort ändern" + +#: templates/Users/index.php:39 +msgid "Edit" +msgstr "Bearbeiten" + +#: templates/Users/index.php:49 +msgid "previous" +msgstr "vorherig" + +#: templates/Users/index.php:51 +msgid "next" +msgstr "nächste" + +#: templates/Users/login.php:19 +msgid "Please enter your username and password" +msgstr "Bitte geben Sie ihren Benutzernamen und Passwort ein" + +#: templates/Users/login.php:29 +msgid "Remember me" +msgstr "Angemeldet bleiben" + +#: templates/Users/login.php:37 +msgid "Register" +msgstr "Registrieren" + +#: templates/Users/login.php:43 +msgid "Reset Password" +msgstr "Passwort zurücksetzen" + +#: templates/Users/login.php:48 +msgid "Login" +msgstr "Anmelden" + +#: templates/Users/profile.php:21 +msgid "{0} {1}" +msgstr "{0} {1}" + +#: templates/Users/profile.php:27 +msgid "Change Password" +msgstr "Passwort ändern" + +#: templates/Users/profile.php:38 templates/Users/view.php:68 +msgid "Social Accounts" +msgstr "Social Accounts" + +#: templates/Users/profile.php:42 templates/Users/view.php:73 +msgid "Avatar" +msgstr "Avatar" + +#: templates/Users/profile.php:43 templates/Users/view.php:72 +msgid "Provider" +msgstr "Provider" + +#: templates/Users/profile.php:44 +msgid "Link" +msgstr "Link" + +#: templates/Users/profile.php:51 +msgid "Link to {0}" +msgstr "Link zu {0}" + +#: templates/Users/register.php:31 +msgid "Accept TOS conditions?" +msgstr "AGBs akzeptieren?" + +#: templates/Users/request_reset_password.php:20 +msgid "Please enter your email or username to reset your password" +msgstr "" +"Bitte geben Sie ihre Email Adresse oder Benutzernamen ein um ihr Passwort " +"zurückzusetzen" + +#: templates/Users/resend_token_validation.php:15 +msgid "Resend Validation email" +msgstr "Validierungs-Email erneut versenden" + +#: templates/Users/resend_token_validation.php:17 +msgid "Email or username" +msgstr "Email oder Benutzername" + +#: templates/Users/u2f_authenticate.php:22 templates/Users/webauthn2fa.php:25 +msgid "Verify your registered yubico key" +msgstr "Bestätige deinen registrierten Yubico Key" + +#: templates/Users/u2f_authenticate.php:23 templates/Users/u2f_register.php:23 +#: templates/Users/webauthn2fa.php:20 templates/Users/webauthn2fa.php:26 +msgid "Please insert and tap your yubico key" +msgstr "Bitte Yubico Key anstecken und antippen" + +#: templates/Users/u2f_authenticate.php:24 templates/Users/webauthn2fa.php:27 +msgid "" +"You can now finish the authentication process using the registered device." +msgstr "" +"Sie können nun den Authentifizierungs-Prozess mit dem registrierten Gerät " +"fertigstellen." + +#: templates/Users/u2f_authenticate.php:25 templates/Users/webauthn2fa.php:22 +#: templates/Users/webauthn2fa.php:28 +msgid "" +"When the YubiKey starts blinking, press the golden disc to activate it. " +"Depending on the web browser you might need to confirm the use of extended " +"information from the YubiKey." +msgstr "" +"Wenn der YubiKey anfängt zu blinken drücken Sie bitte die goldene Diskette " +"um ihn zu aktivieren. Abhängig vom Web-Browser könnten Berechtigungs-Popups " +"erscheinen um den YubiKey zu verwenden." + +#: templates/Users/u2f_authenticate.php:27 templates/Users/u2f_register.php:27 +#: templates/Users/webauthn2fa.php:31 +msgid "Reload" +msgstr "Neu laden" + +#: templates/Users/u2f_authenticate.php:51 templates/Users/u2f_register.php:56 +msgid "Yubico key check has failed, please try again" +msgstr "Yubico Key Überprüfung fehlgeschlagen. Bitte versuchen Sie es erneut" + +#: templates/Users/u2f_register.php:22 templates/Users/webauthn2fa.php:19 +msgid "Registering your yubico key" +msgstr "Yubico Key registrieren" + +#: templates/Users/verify.php:13 +msgid "Verification Code" +msgstr "Bestätigungscode" + +#: templates/Users/verify.php:15 +msgid "" +" " +"Verify" +msgstr "" +" " +"Bestätigen" + +#: templates/Users/view.php:19 +msgid "Delete User" +msgstr "Benutzer löschen" + +#: templates/Users/view.php:24 +msgid "New User" +msgstr "Benutzer hinzufügen" + +#: templates/Users/view.php:31 +msgid "Id" +msgstr "ID" + +#: templates/Users/view.php:37 +msgid "First Name" +msgstr "Vorname" + +#: templates/Users/view.php:39 +msgid "Last Name" +msgstr "Nachname" + +#: templates/Users/view.php:41 +msgid "Role" +msgstr "Rolle" + +#: templates/Users/view.php:45 +msgid "Api Token" +msgstr "API Token" + +#: templates/Users/view.php:53 +msgid "Token Expires" +msgstr "Token Ablaufdatum" + +#: templates/Users/view.php:55 +msgid "Activation Date" +msgstr "Aktivierungsdatum" + +#: templates/Users/view.php:57 +msgid "Tos Date" +msgstr "AGB Datum" + +#: templates/Users/view.php:59 templates/Users/view.php:75 +msgid "Created" +msgstr "erstellt am" + +#: templates/Users/view.php:61 templates/Users/view.php:76 +msgid "Modified" +msgstr "zuletzt aktualisiert" + +#: templates/Users/webauthn2fa.php:8 +msgid "Two-factor authentication" +msgstr "Two-Factor Authentifizierung" + +#: templates/Users/webauthn2fa.php:21 +msgid "" +"In order to enable your YubiKey the first step is to perform a registration." +msgstr "" +"Damit Sie ihren YubiKey verwenden können müssen Sie diesen zuerst " +"registrieren." + +#: templates/email/html/reset_password.php:14 +#: templates/email/html/social_account_validation.php:14 +#: templates/email/html/validation.php:13 +#: templates/email/text/reset_password.php:12 +#: templates/email/text/social_account_validation.php:12 +#: templates/email/text/validation.php:12 +msgid "Hi {0}" +msgstr "Hallo {0}" + +#: templates/email/html/reset_password.php:17 +msgid "Reset your password here" +msgstr "Passwort jetzt zurücksetzen" + +#: templates/email/html/reset_password.php:20 +#: templates/email/html/social_account_validation.php:23 +#: templates/email/html/validation.php:19 +msgid "" +"If the link is not correctly displayed, please copy the following address in " +"your web browser {0}" +msgstr "" +"Falls der Link nicht korrekt dargestellt wird kopieren Sie bitte die " +"folgende Adresse in ihren Web Browser {0}" + +#: templates/email/html/reset_password.php:27 +#: templates/email/html/social_account_validation.php:30 +#: templates/email/html/validation.php:26 +#: templates/email/text/reset_password.php:20 +#: templates/email/text/social_account_validation.php:20 +#: templates/email/text/validation.php:20 +msgid "Thank you" +msgstr "Vielen Dank" + +#: templates/email/html/social_account_validation.php:18 +msgid "Activate your social login here" +msgstr "Social Login aktivieren" + +#: templates/email/html/validation.php:16 +msgid "Activate your account here" +msgstr "Konto aktivieren" + +#: templates/email/text/reset_password.php:14 +#: templates/email/text/validation.php:14 +msgid "Please copy the following address in your web browser {0}" +msgstr "Bitte kopieren Sie die folgende Adresse in Ihren Web Browser {0}" + +#: templates/email/text/social_account_validation.php:14 +msgid "" +"Please copy the following address in your web browser to activate your " +"social login {0}" +msgstr "" +"Bitte kopieren Sie die folgende Adresse in Ihren Web Browser um den Social " +"Login zu aktivieren {0}" diff --git a/resources/locales/uk/users.mo b/resources/locales/uk/users.mo new file mode 100644 index 000000000..074b08d3f Binary files /dev/null and b/resources/locales/uk/users.mo differ diff --git a/resources/locales/uk/users.po b/resources/locales/uk/users.po new file mode 100644 index 000000000..0ea72d3d8 --- /dev/null +++ b/resources/locales/uk/users.po @@ -0,0 +1,800 @@ +# Ukrainian translation of CakePHP Application +# Copyright 2020 Yaroslav Kontsevyi +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"POT-Creation-Date: 2019-01-10 14:38+0000\n" +"PO-Revision-Date: 2020-04-03 14:57+0300\n" +"Language-Team: CakeDC \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);\n" +"X-Generator: Poedit 2.3\n" +"Last-Translator: Yaroslav Kontsevyi \n" +"Language: uk\n" + +#: Controller/SocialAccountsController.php:50 +msgid "Account validated successfully" +msgstr "Обліковий запис успішно підтверджено" + +#: Controller/SocialAccountsController.php:52 +msgid "Account could not be validated" +msgstr "Не вдалося підтвердити обліковий запис" + +#: Controller/SocialAccountsController.php:55 +msgid "Invalid token and/or social account" +msgstr "Недійсний маркер та / або соціальний акаунт" + +#: Controller/SocialAccountsController.php:57;85 +msgid "Social Account already active" +msgstr "Соціальний акаунт вже активний" + +#: Controller/SocialAccountsController.php:59 +msgid "Social Account could not be validated" +msgstr "Не вдалося підтвердити соціальний обліковий запис" + +#: Controller/SocialAccountsController.php:78 +msgid "Email sent successfully" +msgstr "Електронна пошта відправлена успішно" + +#: Controller/SocialAccountsController.php:80 +msgid "Email could not be sent" +msgstr "Не вдалося надіслати електронну пошту" + +#: Controller/SocialAccountsController.php:83 +msgid "Invalid account" +msgstr "Недійсний обліковий запис" + +#: Controller/SocialAccountsController.php:87 +msgid "Email could not be resent" +msgstr "Не вдалося відіслати електронний лист" + +#: Controller/Traits/LinkSocialTrait.php:52 +msgid "Could not associate account, please try again." +msgstr "Неможливо пов’язати обліковий запис. Повторіть спробу." + +#: Controller/Traits/LinkSocialTrait.php:76 +msgid "Social account was associated." +msgstr "Соціальний акаунт успішно активовано." + +#: Controller/Traits/LoginTrait.php:73 +msgid "You've successfully logged out" +msgstr "Ви успішно вийшли" + +#: Controller/Traits/OneTimePasswordVerifyTrait.php:71 +msgid "Please enable Google Authenticator first." +msgstr "Спершу ввімкніть Google Authenticator." + +#: Controller/Traits/OneTimePasswordVerifyTrait.php:80 +msgid "Could not find user data" +msgstr "Не вдалося знайти дані користувача" + +#: Controller/Traits/OneTimePasswordVerifyTrait.php:116 +msgid "Could not verify, please try again" +msgstr "Неможливо підтвердити. Повторіть спробу" + +#: Controller/Traits/OneTimePasswordVerifyTrait.php:144 +msgid "Verification code is invalid. Try again" +msgstr "Код підтвердження недійсний. Спробуйте ще раз" + +#: Controller/Traits/PasswordManagementTrait.php:53;91 +#: Controller/Traits/ProfileTrait.php:53 +msgid "User was not found" +msgstr "Користувача не знайдено" + +#: Controller/Traits/PasswordManagementTrait.php:75;87;95 +msgid "Password could not be changed" +msgstr "Не вдалося змінити пароль" + +#: Controller/Traits/PasswordManagementTrait.php:83 +msgid "Password has been changed successfully" +msgstr "Пароль успішно змінено" + +#: Controller/Traits/PasswordManagementTrait.php:137 +msgid "Please check your email to continue with password reset process" +msgstr "Будь ласка, перевірте вашу електронну пошту для відновлення пароля" + +#: Controller/Traits/PasswordManagementTrait.php:140 Shell/UsersShell.php:247 +msgid "The password token could not be generated. Please try again" +msgstr "Не вдалося створити маркер пароля. Будь ласка спробуйте ще раз" + +#: Controller/Traits/PasswordManagementTrait.php:146 +#: Controller/Traits/UserValidationTrait.php:116 +msgid "User {0} was not found" +msgstr "Користувача {0} не знайдено" + +#: Controller/Traits/PasswordManagementTrait.php:148 +msgid "The user is not active" +msgstr "Користувач неактивний" + +#: Controller/Traits/PasswordManagementTrait.php:150 +#: Controller/Traits/UserValidationTrait.php:111;120 +msgid "Token could not be reset" +msgstr "Не вдалося скинути маркер" + +#: Controller/Traits/PasswordManagementTrait.php:174 +msgid "Google Authenticator token was successfully reset" +msgstr "Маркер Google Authenticator успішно скинуто" + +#: Controller/Traits/ProfileTrait.php:57 +msgid "Not authorized, please login first" +msgstr "Не дозволено, спочатку увійдіть" + +#: Controller/Traits/RegisterTrait.php:46 +msgid "You must log out to register a new user account" +msgstr "Ви повинні вийти з системи, щоб зареєструвати новий обліковий запис" + +#: Controller/Traits/RegisterTrait.php:75;99 +msgid "The user could not be saved" +msgstr "Користувача не вдалося зберегти" + +#: Controller/Traits/RegisterTrait.php:92 Loader/LoginComponentLoader.php:32 +msgid "Invalid reCaptcha" +msgstr "Недійсна reCaptcha" + +#: Controller/Traits/RegisterTrait.php:133 +msgid "You have registered successfully, please log in" +msgstr "Ви успішно зареєструвались, будь ласка, увійдіть" + +#: Controller/Traits/RegisterTrait.php:135 +msgid "Please validate your account before log in" +msgstr "Будь ласка, підтвердьте свій обліковий запис, перш ніж увійти" + +#: Controller/Traits/SimpleCrudTrait.php:77;107 +msgid "The {0} has been saved" +msgstr "Успішно збережено {0}" + +#: Controller/Traits/SimpleCrudTrait.php:81;111 +msgid "The {0} could not be saved" +msgstr "Не вдалося зберегти {0}" + +#: Controller/Traits/SimpleCrudTrait.php:131 +msgid "The {0} has been deleted" +msgstr "{0} видалено" + +#: Controller/Traits/SimpleCrudTrait.php:133 +msgid "The {0} could not be deleted" +msgstr "{0} не може бути видалено" + +#: Controller/Traits/UserValidationTrait.php:44 +msgid "User account validated successfully" +msgstr "Обліковий запис успішно підтверджено" + +#: Controller/Traits/UserValidationTrait.php:46 +msgid "User account could not be validated" +msgstr "Не вдалося підтвердити обліковий запис" + +#: Controller/Traits/UserValidationTrait.php:49 +msgid "User already active" +msgstr "Користувач вже активний" + +#: Controller/Traits/UserValidationTrait.php:55 +msgid "Reset password token was validated successfully" +msgstr "Маркер скидання пароля успішно перевірено" + +#: Controller/Traits/UserValidationTrait.php:63 +msgid "Reset password token could not be validated" +msgstr "Маркер скидання пароля не може бути перевірений" + +#: Controller/Traits/UserValidationTrait.php:67 +msgid "Invalid validation type" +msgstr "Недійсний тип перевірки" + +#: Controller/Traits/UserValidationTrait.php:70 +msgid "Invalid token or user account already validated" +msgstr "Недійсний маркер або обліковий запис користувача вже підтверджено" + +#: Controller/Traits/UserValidationTrait.php:76 +msgid "Token already expired" +msgstr "Маркер вже сплив" + +#: Controller/Traits/UserValidationTrait.php:106 +msgid "Token has been reset successfully. Please check your email." +msgstr "Маркер успішно скинуто. Будь ласка, перевірте свою електронну пошту." + +#: Controller/Traits/UserValidationTrait.php:118 +msgid "User {0} is already active" +msgstr "Користувач {0} вже активний" + +#: Loader/LoginComponentLoader.php:30 +msgid "Username or password is incorrect" +msgstr "Ім'я користувача або пароль невірні" + +#: Loader/LoginComponentLoader.php:51 +msgid "Could not proceed with social account. Please try again" +msgstr "" +"Не вдалось продовжити використання соціального облікового запису. Будь ласка " +"спробуйте ще раз" + +#: Loader/LoginComponentLoader.php:53 +msgid "" +"Your user has not been validated yet. Please check your inbox for " +"instructions" +msgstr "" +"Користувача ще не підтверджено. Будь ласка, перевірте вашу поштову скриньку " +"для інструкцій" + +#: Loader/LoginComponentLoader.php:57 +msgid "" +"Your social account has not been validated yet. Please check your inbox for " +"instructions" +msgstr "" +"Ваш соціальний обліковий запис ще не перевірено. Будь ласка, перевірте вашу " +"поштову скриньку для інструкцій" + +#: Mailer/UsersMailer.php:33 +msgid "Your account validation link" +msgstr "Посилання для перевірки облікового запису" + +#: Mailer/UsersMailer.php:51 +msgid "{0}Your reset password link" +msgstr "{0} Ваше посилання для скидання пароля" + +#: Mailer/UsersMailer.php:74 +msgid "{0}Your social account validation link" +msgstr "{0} Посилання для перевірки облікового запису" + +#: Middleware/SocialAuthMiddleware.php:65 Template/Users/social_email.ctp:16 +msgid "Please enter your email" +msgstr "Введіть адресу електронної пошти" + +#: Middleware/SocialAuthMiddleware.php:75 +msgid "Could not identify your account, please try again" +msgstr "Не вдалося визначити обліковий запис, будь ласка, спробуйте ще раз" + +#: Model/Behavior/AuthFinderBehavior.php:48 +msgid "Missing 'username' in options data" +msgstr "«Ім'я користувача» відсутнє в параметрах даних" + +#: Model/Behavior/LinkSocialBehavior.php:53 +msgid "Social account already associated to another user" +msgstr "Соціальний обліковий запис, вже пов'язаний з іншим користувачем" + +#: Model/Behavior/PasswordBehavior.php:45 +msgid "Reference cannot be null" +msgstr "Посилання не може бути пустим" + +#: Model/Behavior/PasswordBehavior.php:50 +msgid "Token expiration cannot be empty" +msgstr "Термін дії маркера не може бути пустим" + +#: Model/Behavior/PasswordBehavior.php:56;138 +msgid "User not found" +msgstr "Користувача не знайдено" + +#: Model/Behavior/PasswordBehavior.php:60 +#: Model/Behavior/RegisterBehavior.php:112 +msgid "User account already validated" +msgstr "Обліковий запис користувача вже підтверджено" + +#: Model/Behavior/PasswordBehavior.php:67 +msgid "User not active" +msgstr "Користувач не активний" + +#: Model/Behavior/PasswordBehavior.php:143 +msgid "The current password does not match" +msgstr "Поточний пароль не збігається" + +#: Model/Behavior/PasswordBehavior.php:146 +msgid "You cannot use the current password as the new one" +msgstr "Не можна використовувати поточний пароль як новий" + +#: Model/Behavior/RegisterBehavior.php:90 +msgid "User not found for the given token and email." +msgstr "Не знайдено користувача з цим маркером та адресою електронної пошти." + +#: Model/Behavior/RegisterBehavior.php:93 +msgid "Token has already expired user with no token" +msgstr "Маркер вже сплив, користувач без маркера" + +#: Model/Behavior/RegisterBehavior.php:151 +msgid "This field is required" +msgstr "Це поле обов‘язкове" + +#: Model/Behavior/SocialAccountBehavior.php:102;129 +msgid "Account already validated" +msgstr "Обліковий запис вже підтверджено" + +#: Model/Behavior/SocialAccountBehavior.php:105;132 +msgid "Account not found for the given token and email." +msgstr "Не знайдено користувача з цим маркером та адресою електронної пошти." + +#: Model/Behavior/SocialBehavior.php:83 +msgid "Unable to login user with reference {0}" +msgstr "Не вдається ввійти за посиланням {0}" + +#: Model/Behavior/SocialBehavior.php:122 +msgid "Email not present" +msgstr "Електронна пошта відсутня" + +#: Model/Table/UsersTable.php:79 +msgid "Your password does not match your confirm password. Please try again" +msgstr "Ваш пароль не відповідає підтвердженню. Будь ласка, спробуйте ще раз" + +#: Model/Table/UsersTable.php:171 +msgid "Username already exists" +msgstr "Такий користувач вже існує" + +#: Model/Table/UsersTable.php:177 +msgid "Email already exists" +msgstr "Електронна пошта вже існує" + +#: Shell/UsersShell.php:58 +msgid "Utilities for CakeDC Users Plugin" +msgstr "Утиліти для плагіна CakeDC Users" + +#: Shell/UsersShell.php:60 +msgid "Activate an specific user" +msgstr "Активація конкретного користувача" + +#: Shell/UsersShell.php:63 +msgid "Add a new superadmin user for testing purposes" +msgstr "Додавання нового користувача superadmin для цілей тестування" + +#: Shell/UsersShell.php:66 +msgid "Add a new user" +msgstr "Додати нового користувача" + +#: Shell/UsersShell.php:69 +msgid "Change the role for an specific user" +msgstr "Змінення ролі для певного користувача" + +#: Shell/UsersShell.php:72 +msgid "Deactivate an specific user" +msgstr "Деактивувати конкретного користувача" + +#: Shell/UsersShell.php:75 +msgid "Delete an specific user" +msgstr "Видалення конкретного користувача" + +#: Shell/UsersShell.php:78 +msgid "Reset the password via email" +msgstr "Скинути пароль за допомогою електронної пошти" + +#: Shell/UsersShell.php:81 +msgid "Reset the password for all users" +msgstr "Скидання пароля для всіх користувачів" + +#: Shell/UsersShell.php:84 +msgid "Reset the password for an specific user" +msgstr "Скидання пароля для певного користувача" + +#: Shell/UsersShell.php:133;159 +msgid "Please enter a password." +msgstr "Будь ласка, введіть пароль." + +#: Shell/UsersShell.php:137 +msgid "Password changed for all users" +msgstr "Пароль змінено для всіх користувачів" + +#: Shell/UsersShell.php:138;166 +msgid "New password: {0}" +msgstr "Новий пароль: {0}" + +#: Shell/UsersShell.php:156;184;262;359 +msgid "Please enter a username." +msgstr "Введіть ім'я користувача." + +#: Shell/UsersShell.php:165 +msgid "Password changed for user: {0}" +msgstr "Пароль змінено для користувача: {0}" + +#: Shell/UsersShell.php:187 +msgid "Please enter a role." +msgstr "Будь ласка, введіть роль." + +#: Shell/UsersShell.php:193 +msgid "Role changed for user: {0}" +msgstr "Роль змінено для користувача: {0}" + +#: Shell/UsersShell.php:194 +msgid "New role: {0}" +msgstr "Нова роль: {0}" + +#: Shell/UsersShell.php:209 +msgid "User was activated: {0}" +msgstr "Користувач був активований: {0}" + +#: Shell/UsersShell.php:224 +msgid "User was de-activated: {0}" +msgstr "Користувач був де-активований: {0}" + +#: Shell/UsersShell.php:236 +msgid "Please enter a username or email." +msgstr "Введіть ім'я користувача або email." + +#: Shell/UsersShell.php:244 +msgid "" +"Please ask the user to check the email to continue with password reset " +"process" +msgstr "" +"Будь ласка, зверніться до користувача, щоб перевірити електронну пошту для " +"продовження процесу скидання пароля" + +#: Shell/UsersShell.php:308 +msgid "Superuser added:" +msgstr "Superuser-а додано:" + +#: Shell/UsersShell.php:310 +msgid "User added:" +msgstr "Користувача додано:" + +#: Shell/UsersShell.php:312 +msgid "Id: {0}" +msgstr "Id: {0}" + +#: Shell/UsersShell.php:313 +msgid "Username: {0}" +msgstr "Користувач: {0}" + +#: Shell/UsersShell.php:314 +msgid "Email: {0}" +msgstr "Електронна пошта: {0}" + +#: Shell/UsersShell.php:315 +msgid "Role: {0}" +msgstr "Роль: {0}" + +#: Shell/UsersShell.php:316 +msgid "Password: {0}" +msgstr "Пароль: {0}" + +#: Shell/UsersShell.php:318 +msgid "User could not be added:" +msgstr "Не вдалося додати користувача:" + +#: Shell/UsersShell.php:321 +msgid "Field: {0} Error: {1}" +msgstr "Поле: {0} Помилка: {1}" + +#: Shell/UsersShell.php:337 +msgid "The user was not found." +msgstr "Користувача не знайдено." + +#: Shell/UsersShell.php:367 +msgid "The user {0} was not deleted. Please try again" +msgstr "Користувача {0} не видалено. Будь ласка, спробуйте ще раз" + +#: Shell/UsersShell.php:369 +msgid "The user {0} was deleted successfully" +msgstr "Користувача {0} успішно видалено" + +#: Template/Email/html/reset_password.ctp:21 +#: Template/Email/html/social_account_validation.ctp:14 +#: Template/Email/html/validation.ctp:21 +#: Template/Email/text/reset_password.ctp:20 +#: Template/Email/text/social_account_validation.ctp:22 +#: Template/Email/text/validation.ctp:20 +msgid "Hi {0}" +msgstr "Вітаємо {0}" + +#: Template/Email/html/reset_password.ctp:24 +msgid "Reset your password here" +msgstr "Скинути пароль" + +#: Template/Email/html/reset_password.ctp:27 +#: Template/Email/html/social_account_validation.ctp:32 +#: Template/Email/html/validation.ctp:27 +msgid "" +"If the link is not correctly displayed, please copy the following address in " +"your web browser {0}" +msgstr "" +"Якщо посилання не відображається належним чином, скопіюйте наступну адресу у " +"веб-переглядач {0}" + +#: Template/Email/html/reset_password.ctp:34 +#: Template/Email/html/social_account_validation.ctp:39 +#: Template/Email/html/validation.ctp:34 +#: Template/Email/text/reset_password.ctp:28 +#: Template/Email/text/social_account_validation.ctp:30 +#: Template/Email/text/validation.ctp:28 +msgid "Thank you" +msgstr "Дякуємо" + +#: Template/Email/html/social_account_validation.ctp:18 +msgid "Activate your social login here" +msgstr "Активуйте свій соціальний логін тут" + +#: Template/Email/html/validation.ctp:24 +msgid "Activate your account here" +msgstr "Активуйте свій акаунт тут" + +#: Template/Email/text/reset_password.ctp:22 +#: Template/Email/text/validation.ctp:22 +msgid "Please copy the following address in your web browser {0}" +msgstr "Будь ласка, скопіюйте наступну адресу у веб-переглядач {0}" + +#: Template/Email/text/social_account_validation.ctp:24 +msgid "" +"Please copy the following address in your web browser to activate your " +"social login {0}" +msgstr "" +"Будь ласка, скопіюйте наступну адресу у веб-браузер, щоб активувати ваш " +"соціальний логін {0}" + +#: Template/Users/add.ctp:13 Template/Users/edit.ctp:17 +#: Template/Users/index.ctp:13;26 Template/Users/view.ctp:15 +msgid "Actions" +msgstr "Дії" + +#: Template/Users/add.ctp:15 Template/Users/edit.ctp:28 +#: Template/Users/view.ctp:23 +msgid "List Users" +msgstr "Перелік користувачів" + +#: Template/Users/add.ctp:21 Template/Users/register.ctp:18 +msgid "Add User" +msgstr "Створити користувача" + +#: Template/Users/add.ctp:23 Template/Users/edit.ctp:36 +#: Template/Users/index.ctp:22 Template/Users/login.ctp:20 +#: Template/Users/profile.ctp:30 Template/Users/register.ctp:20 +#: Template/Users/view.ctp:33 +msgid "Username" +msgstr "Iм'я користувача" + +#: Template/Users/add.ctp:24 Template/Users/edit.ctp:37 +#: Template/Users/index.ctp:23 Template/Users/profile.ctp:32 +#: Template/Users/register.ctp:21 Template/Users/view.ctp:35 +msgid "Email" +msgstr "Електронна пошта" + +#: Template/Users/add.ctp:25 Template/Users/login.ctp:21 +#: Template/Users/register.ctp:22 +msgid "Password" +msgstr "Пароль" + +#: Template/Users/add.ctp:26 Template/Users/edit.ctp:38 +#: Template/Users/index.ctp:24 Template/Users/register.ctp:27 +msgid "First name" +msgstr "Ім‘я" + +#: Template/Users/add.ctp:27 Template/Users/edit.ctp:39 +#: Template/Users/index.ctp:25 Template/Users/register.ctp:28 +msgid "Last name" +msgstr "Прізвище" + +#: Template/Users/add.ctp:30 Template/Users/edit.ctp:54 +#: Template/Users/view.ctp:49;74 +msgid "Active" +msgstr "Активний" + +#: Template/Users/add.ctp:34 Template/Users/change_password.ctp:25 +#: Template/Users/edit.ctp:58 Template/Users/register.ctp:37 +#: Template/Users/request_reset_password.ctp:8 +#: Template/Users/resend_token_validation.ctp:20 +#: Template/Users/social_email.ctp:19 +msgid "Submit" +msgstr "Відправити" + +#: Template/Users/change_password.ctp:5 +msgid "Please enter the new password" +msgstr "Будь ласка, введіть пароль" + +#: Template/Users/change_password.ctp:10 +msgid "Current password" +msgstr "Теперішний пароль" + +#: Template/Users/change_password.ctp:16 +msgid "New password" +msgstr "Новий пароль" + +#: Template/Users/change_password.ctp:21 Template/Users/register.ctp:25 +msgid "Confirm password" +msgstr "Повторити пароль" + +#: Template/Users/edit.ctp:22 Template/Users/index.ctp:40 +msgid "Delete" +msgstr "Видалити" + +#: Template/Users/edit.ctp:24 Template/Users/index.ctp:40 +#: Template/Users/view.ctp:21 +msgid "Are you sure you want to delete # {0}?" +msgstr "Впевнені, що хочете видалити # {0}?" + +#: Template/Users/edit.ctp:34 Template/Users/view.ctp:17 +msgid "Edit User" +msgstr "Редагувати користувача" + +#: Template/Users/edit.ctp:40 Template/Users/view.ctp:43 +msgid "Token" +msgstr "Маркер" + +#: Template/Users/edit.ctp:42 +msgid "Token expires" +msgstr "Маркер спилв" + +#: Template/Users/edit.ctp:45 +msgid "API token" +msgstr "Маркер API" + +#: Template/Users/edit.ctp:48 +msgid "Activation date" +msgstr "Дата активації" + +#: Template/Users/edit.ctp:51 +msgid "TOS date" +msgstr "Дата приймання умов" + +#: Template/Users/edit.ctp:64 +msgid "Reset Google Authenticator Token" +msgstr "Скинути маркер Google Authenticator" + +#: Template/Users/edit.ctp:70 +msgid "Are you sure you want to reset token for user \"{0}\"?" +msgstr "Ви дійсно бажаєте скинути маркер для користувача \"{0}\"?" + +#: Template/Users/index.ctp:15 +msgid "New {0}" +msgstr "Створити {0}" + +#: Template/Users/index.ctp:37 +msgid "View" +msgstr "Перегляд" + +#: Template/Users/index.ctp:38 +msgid "Change password" +msgstr "Змінити пароль" + +#: Template/Users/index.ctp:39 +msgid "Edit" +msgstr "Редагувати" + +#: Template/Users/index.ctp:49 +msgid "previous" +msgstr "попередня" + +#: Template/Users/index.ctp:51 +msgid "next" +msgstr "наступна" + +#: Template/Users/login.ctp:19 +msgid "Please enter your username and password" +msgstr "Введіть ім'я користувача і пароль" + +#: Template/Users/login.ctp:29 +msgid "Remember me" +msgstr "Запам‘ятати мене" + +#: Template/Users/login.ctp:37 +msgid "Register" +msgstr "Реєстрація" + +#: Template/Users/login.ctp:43 +msgid "Reset Password" +msgstr "Скинути пароль" + +#: Template/Users/login.ctp:48 +msgid "Login" +msgstr "Ввійти" + +#: Template/Users/profile.ctp:21 +msgid "{0} {1}" +msgstr "{0} {1}" + +#: Template/Users/profile.ctp:27 +msgid "Change Password" +msgstr "Змінити пароль" + +#: Template/Users/profile.ctp:38 Template/Users/view.ctp:68 +msgid "Social Accounts" +msgstr "Соціальні акаунти" + +#: Template/Users/profile.ctp:42 Template/Users/view.ctp:73 +msgid "Avatar" +msgstr "Аватар" + +#: Template/Users/profile.ctp:43 Template/Users/view.ctp:72 +msgid "Provider" +msgstr "Провайдер" + +#: Template/Users/profile.ctp:44 +msgid "Link" +msgstr "Посилання" + +#: Template/Users/profile.ctp:51 +msgid "Link to {0}" +msgstr "Посилання на {0}" + +#: Template/Users/register.ctp:30 +msgid "Accept TOS conditions?" +msgstr "Приймаєте умови сервісу?" + +#: Template/Users/request_reset_password.ctp:5 +msgid "Please enter your email to reset your password" +msgstr "Введіть адресу електронної пошти для скидання пароля" + +#: Template/Users/resend_token_validation.ctp:15 +msgid "Resend Validation email" +msgstr "Надислати код підтвердження ще раз" + +#: Template/Users/resend_token_validation.ctp:17 +msgid "Email or username" +msgstr "Email або ім'я користувача" + +#: Template/Users/verify.ctp:13 +msgid "Verification Code" +msgstr "Код підтвердження" + +#: Template/Users/verify.ctp:15 +msgid "" +" " +"Verify" +msgstr "" +" " +"Перевірити" + +#: Template/Users/view.ctp:19 +msgid "Delete User" +msgstr "Видалити користувача" + +#: Template/Users/view.ctp:24 +msgid "New User" +msgstr "Новий користувач" + +#: Template/Users/view.ctp:31 +msgid "Id" +msgstr "Id" + +#: Template/Users/view.ctp:37 +msgid "First Name" +msgstr "Ім‘я" + +#: Template/Users/view.ctp:39 +msgid "Last Name" +msgstr "Прізвище" + +#: Template/Users/view.ctp:41 +msgid "Role" +msgstr "Роль" + +#: Template/Users/view.ctp:45 +msgid "Api Token" +msgstr "Маркер API" + +#: Template/Users/view.ctp:53 +msgid "Token Expires" +msgstr "Маркер спливає" + +#: Template/Users/view.ctp:55 +msgid "Activation Date" +msgstr "Дата активації" + +#: Template/Users/view.ctp:57 +msgid "Tos Date" +msgstr "Дата приймання умов" + +#: Template/Users/view.ctp:59;75 +msgid "Created" +msgstr "Створено" + +#: Template/Users/view.ctp:61;76 +msgid "Modified" +msgstr "Змінено" + +#: View/Helper/UserHelper.php:46 +msgid "Sign in with" +msgstr "Увійти з використанням" + +#: View/Helper/UserHelper.php:103 +msgid "Logout" +msgstr "Вихiд" + +#: View/Helper/UserHelper.php:121 +msgid "Welcome, {0}" +msgstr "Вітаємо, {0}" + +#: View/Helper/UserHelper.php:151 +msgid "reCaptcha is not configured! Please configure Users.reCaptcha.key" +msgstr "reCaptcha не налаштовано! Налаштуйте Users.reCaptcha.key" + +#: View/Helper/UserHelper.php:215 +msgid "Connected with {0}" +msgstr "Під‘єднано до {0}" + +#: View/Helper/UserHelper.php:220 +msgid "Connect with {0}" +msgstr "Під‘єднати до {0}" diff --git a/resources/locales/users.pot b/resources/locales/users.pot index 59f36b747..c2992ab07 100644 --- a/resources/locales/users.pot +++ b/resources/locales/users.pot @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" -"POT-Creation-Date: 2019-01-10 14:38+0000\n" +"POT-Creation-Date: 2022-03-11 13:46+0100\n" "PO-Revision-Date: YYYY-mm-DD HH:MM+ZZZZ\n" "Last-Translator: NAME \n" "Language-Team: LANGUAGE \n" @@ -14,789 +14,907 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" -#: Controller/SocialAccountsController.php:50 +#: src/Controller/SocialAccountsController.php:50 msgid "Account validated successfully" msgstr "" -#: Controller/SocialAccountsController.php:52 +#: src/Controller/SocialAccountsController.php:52 msgid "Account could not be validated" msgstr "" -#: Controller/SocialAccountsController.php:55 +#: src/Controller/SocialAccountsController.php:55 msgid "Invalid token and/or social account" msgstr "" -#: Controller/SocialAccountsController.php:57;85 +#: src/Controller/SocialAccountsController.php:57 +#: src/Controller/SocialAccountsController.php:85 msgid "Social Account already active" msgstr "" -#: Controller/SocialAccountsController.php:59 +#: src/Controller/SocialAccountsController.php:59 msgid "Social Account could not be validated" msgstr "" -#: Controller/SocialAccountsController.php:78 +#: src/Controller/SocialAccountsController.php:78 msgid "Email sent successfully" msgstr "" -#: Controller/SocialAccountsController.php:80 +#: src/Controller/SocialAccountsController.php:80 msgid "Email could not be sent" msgstr "" -#: Controller/SocialAccountsController.php:83 +#: src/Controller/SocialAccountsController.php:83 msgid "Invalid account" msgstr "" -#: Controller/SocialAccountsController.php:87 +#: src/Controller/SocialAccountsController.php:87 msgid "Email could not be resent" msgstr "" -#: Controller/Traits/LinkSocialTrait.php:52 +#: src/Controller/Traits/LinkSocialTrait.php:56 msgid "Could not associate account, please try again." msgstr "" -#: Controller/Traits/LinkSocialTrait.php:76 +#: src/Controller/Traits/LinkSocialTrait.php:80 msgid "Social account was associated." msgstr "" -#: Controller/Traits/LoginTrait.php:73 +#: src/Controller/Traits/LoginTrait.php:73 msgid "You've successfully logged out" msgstr "" -#: Controller/Traits/OneTimePasswordVerifyTrait.php:71 +#: src/Controller/Traits/OneTimePasswordVerifyTrait.php:75 msgid "Please enable Google Authenticator first." msgstr "" -#: Controller/Traits/OneTimePasswordVerifyTrait.php:80 +#: src/Controller/Traits/OneTimePasswordVerifyTrait.php:90 msgid "Could not find user data" msgstr "" -#: Controller/Traits/OneTimePasswordVerifyTrait.php:116 +#: src/Controller/Traits/OneTimePasswordVerifyTrait.php:129 msgid "Could not verify, please try again" msgstr "" -#: Controller/Traits/OneTimePasswordVerifyTrait.php:144 +#: src/Controller/Traits/OneTimePasswordVerifyTrait.php:161 msgid "Verification code is invalid. Try again" msgstr "" -#: Controller/Traits/PasswordManagementTrait.php:53;91 -#: Controller/Traits/ProfileTrait.php:53 +#: src/Controller/Traits/PasswordManagementTrait.php:63 +msgid "Changing another user's password is not allowed" +msgstr "" + +#: src/Controller/Traits/PasswordManagementTrait.php:77 +#: src/Controller/Traits/PasswordManagementTrait.php:121 +#: src/Controller/Traits/ProfileTrait.php:54 msgid "User was not found" msgstr "" -#: Controller/Traits/PasswordManagementTrait.php:75;87;95 +#: src/Controller/Traits/PasswordManagementTrait.php:105 +#: src/Controller/Traits/PasswordManagementTrait.php:117 +#: src/Controller/Traits/PasswordManagementTrait.php:125 msgid "Password could not be changed" msgstr "" -#: Controller/Traits/PasswordManagementTrait.php:83 +#: src/Controller/Traits/PasswordManagementTrait.php:113 msgid "Password has been changed successfully" msgstr "" -#: Controller/Traits/PasswordManagementTrait.php:137 +#: src/Controller/Traits/PasswordManagementTrait.php:167 msgid "Please check your email to continue with password reset process" msgstr "" -#: Controller/Traits/PasswordManagementTrait.php:140 -#: Shell/UsersShell.php:247 +#: src/Controller/Traits/PasswordManagementTrait.php:170 +#: src/Shell/UsersShell.php:286 msgid "The password token could not be generated. Please try again" msgstr "" -#: Controller/Traits/PasswordManagementTrait.php:146 -#: Controller/Traits/UserValidationTrait.php:116 +#: src/Controller/Traits/PasswordManagementTrait.php:176 +#: src/Controller/Traits/UserValidationTrait.php:124 msgid "User {0} was not found" msgstr "" -#: Controller/Traits/PasswordManagementTrait.php:148 +#: src/Controller/Traits/PasswordManagementTrait.php:178 msgid "The user is not active" msgstr "" -#: Controller/Traits/PasswordManagementTrait.php:150 -#: Controller/Traits/UserValidationTrait.php:111;120 +#: src/Controller/Traits/PasswordManagementTrait.php:180 +#: src/Controller/Traits/UserValidationTrait.php:119 +#: src/Controller/Traits/UserValidationTrait.php:128 msgid "Token could not be reset" msgstr "" -#: Controller/Traits/PasswordManagementTrait.php:174 +#: src/Controller/Traits/PasswordManagementTrait.php:204 msgid "Google Authenticator token was successfully reset" msgstr "" -#: Controller/Traits/ProfileTrait.php:57 +#: src/Controller/Traits/PasswordManagementTrait.php:207 +msgid "Could not reset Google Authenticator" +msgstr "" + +#: src/Controller/Traits/ProfileTrait.php:58 msgid "Not authorized, please login first" msgstr "" -#: Controller/Traits/RegisterTrait.php:46 +#: src/Controller/Traits/RegisterTrait.php:47 msgid "You must log out to register a new user account" msgstr "" -#: Controller/Traits/RegisterTrait.php:75;99 +#: src/Controller/Traits/RegisterTrait.php:86 +#: src/Controller/Traits/RegisterTrait.php:117 msgid "The user could not be saved" msgstr "" -#: Controller/Traits/RegisterTrait.php:92 -#: Loader/LoginComponentLoader.php:32 +#: src/Controller/Traits/RegisterTrait.php:103 +#: src/Loader/LoginComponentLoader.php:33 msgid "Invalid reCaptcha" msgstr "" -#: Controller/Traits/RegisterTrait.php:133 +#: src/Controller/Traits/RegisterTrait.php:151 msgid "You have registered successfully, please log in" msgstr "" -#: Controller/Traits/RegisterTrait.php:135 +#: src/Controller/Traits/RegisterTrait.php:153 msgid "Please validate your account before log in" msgstr "" -#: Controller/Traits/SimpleCrudTrait.php:77;107 +#: src/Controller/Traits/SimpleCrudTrait.php:77 +#: src/Controller/Traits/SimpleCrudTrait.php:107 msgid "The {0} has been saved" msgstr "" -#: Controller/Traits/SimpleCrudTrait.php:81;111 +#: src/Controller/Traits/SimpleCrudTrait.php:81 +#: src/Controller/Traits/SimpleCrudTrait.php:111 msgid "The {0} could not be saved" msgstr "" -#: Controller/Traits/SimpleCrudTrait.php:131 +#: src/Controller/Traits/SimpleCrudTrait.php:131 msgid "The {0} has been deleted" msgstr "" -#: Controller/Traits/SimpleCrudTrait.php:133 +#: src/Controller/Traits/SimpleCrudTrait.php:133 msgid "The {0} could not be deleted" msgstr "" -#: Controller/Traits/UserValidationTrait.php:44 +#: src/Controller/Traits/U2fTrait.php:216 +msgid "U2F requires SSL." +msgstr "" + +#: src/Controller/Traits/UserValidationTrait.php:49 msgid "User account validated successfully" msgstr "" -#: Controller/Traits/UserValidationTrait.php:46 +#: src/Controller/Traits/UserValidationTrait.php:51 msgid "User account could not be validated" msgstr "" -#: Controller/Traits/UserValidationTrait.php:49 +#: src/Controller/Traits/UserValidationTrait.php:54 msgid "User already active" msgstr "" -#: Controller/Traits/UserValidationTrait.php:55 +#: src/Controller/Traits/UserValidationTrait.php:60 msgid "Reset password token was validated successfully" msgstr "" -#: Controller/Traits/UserValidationTrait.php:63 +#: src/Controller/Traits/UserValidationTrait.php:68 msgid "Reset password token could not be validated" msgstr "" -#: Controller/Traits/UserValidationTrait.php:67 +#: src/Controller/Traits/UserValidationTrait.php:72 msgid "Invalid validation type" msgstr "" -#: Controller/Traits/UserValidationTrait.php:70 +#: src/Controller/Traits/UserValidationTrait.php:75 msgid "Invalid token or user account already validated" msgstr "" -#: Controller/Traits/UserValidationTrait.php:76 +#: src/Controller/Traits/UserValidationTrait.php:81 msgid "Token already expired" msgstr "" -#: Controller/Traits/UserValidationTrait.php:106 +#: src/Controller/Traits/UserValidationTrait.php:114 msgid "Token has been reset successfully. Please check your email." msgstr "" -#: Controller/Traits/UserValidationTrait.php:118 +#: src/Controller/Traits/UserValidationTrait.php:126 msgid "User {0} is already active" msgstr "" -#: Loader/LoginComponentLoader.php:30 +#: src/Controller/Traits/Webauthn2faTrait.php:53 +#: src/Controller/Traits/Webauthn2faTrait.php:73 +msgid "User already has configured webauthn2fa" +msgstr "" + +#: src/Controller/Traits/Webauthn2faTrait.php:77 +#: src/Controller/Traits/Webauthn2faTrait.php:122 +msgid "Register error with webauthn for user id: {0}" +msgstr "" + +#: src/Loader/AuthenticationServiceLoader.php:109 +msgid "Property {0}.className should be defined" +msgstr "" + +#: src/Loader/LoginComponentLoader.php:31 msgid "Username or password is incorrect" msgstr "" -#: Loader/LoginComponentLoader.php:51 +#: src/Loader/LoginComponentLoader.php:52 msgid "Could not proceed with social account. Please try again" msgstr "" -#: Loader/LoginComponentLoader.php:53 +#: src/Loader/LoginComponentLoader.php:54 msgid "Your user has not been validated yet. Please check your inbox for instructions" msgstr "" -#: Loader/LoginComponentLoader.php:57 +#: src/Loader/LoginComponentLoader.php:58 msgid "Your social account has not been validated yet. Please check your inbox for instructions" msgstr "" -#: Mailer/UsersMailer.php:33 +#: src/Mailer/UsersMailer.php:36 msgid "Your account validation link" msgstr "" -#: Mailer/UsersMailer.php:51 +#: src/Mailer/UsersMailer.php:63 msgid "{0}Your reset password link" msgstr "" -#: Mailer/UsersMailer.php:74 +#: src/Mailer/UsersMailer.php:95 msgid "{0}Your social account validation link" msgstr "" -#: Middleware/SocialAuthMiddleware.php:65 -#: Template/Users/social_email.ctp:16 +#: src/Middleware/SocialAuthMiddleware.php:47 +#: templates/Users/social_email.php:16 msgid "Please enter your email" msgstr "" -#: Middleware/SocialAuthMiddleware.php:75 +#: src/Middleware/SocialAuthMiddleware.php:57 msgid "Could not identify your account, please try again" msgstr "" -#: Model/Behavior/AuthFinderBehavior.php:48 +#: src/Middleware/UnauthorizedHandler/DefaultRedirectHandler.php:112 +msgid "You are not authorized to access that location." +msgstr "" + +#: src/Model/Behavior/AuthFinderBehavior.php:49 msgid "Missing 'username' in options data" msgstr "" -#: Model/Behavior/LinkSocialBehavior.php:53 +#: src/Model/Behavior/LinkSocialBehavior.php:52 msgid "Social account already associated to another user" msgstr "" -#: Model/Behavior/PasswordBehavior.php:45 +#: src/Model/Behavior/PasswordBehavior.php:45 msgid "Reference cannot be null" msgstr "" -#: Model/Behavior/PasswordBehavior.php:50 +#: src/Model/Behavior/PasswordBehavior.php:50 msgid "Token expiration cannot be empty" msgstr "" -#: Model/Behavior/PasswordBehavior.php:56;138 +#: src/Model/Behavior/PasswordBehavior.php:56 +#: src/Model/Behavior/PasswordBehavior.php:136 msgid "User not found" msgstr "" -#: Model/Behavior/PasswordBehavior.php:60 -#: Model/Behavior/RegisterBehavior.php:112 +#: src/Model/Behavior/PasswordBehavior.php:60 +#: src/Model/Behavior/RegisterBehavior.php:129 msgid "User account already validated" msgstr "" -#: Model/Behavior/PasswordBehavior.php:67 +#: src/Model/Behavior/PasswordBehavior.php:66 msgid "User not active" msgstr "" -#: Model/Behavior/PasswordBehavior.php:143 +#: src/Model/Behavior/PasswordBehavior.php:141 msgid "The current password does not match" msgstr "" -#: Model/Behavior/PasswordBehavior.php:146 +#: src/Model/Behavior/PasswordBehavior.php:144 msgid "You cannot use the current password as the new one" msgstr "" -#: Model/Behavior/RegisterBehavior.php:90 +#: src/Model/Behavior/RegisterBehavior.php:107 msgid "User not found for the given token and email." msgstr "" -#: Model/Behavior/RegisterBehavior.php:93 +#: src/Model/Behavior/RegisterBehavior.php:110 msgid "Token has already expired user with no token" msgstr "" -#: Model/Behavior/RegisterBehavior.php:151 +#: src/Model/Behavior/RegisterBehavior.php:167 msgid "This field is required" msgstr "" -#: Model/Behavior/SocialAccountBehavior.php:102;129 +#: src/Model/Behavior/SocialAccountBehavior.php:100 +#: src/Model/Behavior/SocialAccountBehavior.php:130 msgid "Account already validated" msgstr "" -#: Model/Behavior/SocialAccountBehavior.php:105;132 +#: src/Model/Behavior/SocialAccountBehavior.php:104 +#: src/Model/Behavior/SocialAccountBehavior.php:135 msgid "Account not found for the given token and email." msgstr "" -#: Model/Behavior/SocialBehavior.php:83 +#: src/Model/Behavior/SocialBehavior.php:85 msgid "Unable to login user with reference {0}" msgstr "" -#: Model/Behavior/SocialBehavior.php:122 +#: src/Model/Behavior/SocialBehavior.php:136 msgid "Email not present" msgstr "" -#: Model/Table/UsersTable.php:79 +#: src/Model/Table/UsersTable.php:107 msgid "Your password does not match your confirm password. Please try again" msgstr "" -#: Model/Table/UsersTable.php:171 +#: src/Model/Table/UsersTable.php:201 msgid "Username already exists" msgstr "" -#: Model/Table/UsersTable.php:177 +#: src/Model/Table/UsersTable.php:207 msgid "Email already exists" msgstr "" -#: Shell/UsersShell.php:58 +#: src/Shell/UsersShell.php:46 msgid "Utilities for CakeDC Users Plugin" msgstr "" -#: Shell/UsersShell.php:60 +#: src/Shell/UsersShell.php:48 msgid "Activate an specific user" msgstr "" -#: Shell/UsersShell.php:63 +#: src/Shell/UsersShell.php:51 msgid "Add a new superadmin user for testing purposes" msgstr "" -#: Shell/UsersShell.php:66 +#: src/Shell/UsersShell.php:54 msgid "Add a new user" msgstr "" -#: Shell/UsersShell.php:69 +#: src/Shell/UsersShell.php:57 msgid "Change the role for an specific user" msgstr "" -#: Shell/UsersShell.php:72 +#: src/Shell/UsersShell.php:60 +msgid "Change the api token for an specific user" +msgstr "" + +#: src/Shell/UsersShell.php:63 msgid "Deactivate an specific user" msgstr "" -#: Shell/UsersShell.php:75 +#: src/Shell/UsersShell.php:66 msgid "Delete an specific user" msgstr "" -#: Shell/UsersShell.php:78 +#: src/Shell/UsersShell.php:69 msgid "Reset the password via email" msgstr "" -#: Shell/UsersShell.php:81 +#: src/Shell/UsersShell.php:72 msgid "Reset the password for all users" msgstr "" -#: Shell/UsersShell.php:84 +#: src/Shell/UsersShell.php:75 msgid "Reset the password for an specific user" msgstr "" -#: Shell/UsersShell.php:133;159 +#: src/Shell/UsersShell.php:135 +#: src/Shell/UsersShell.php:161 msgid "Please enter a password." msgstr "" -#: Shell/UsersShell.php:137 +#: src/Shell/UsersShell.php:139 msgid "Password changed for all users" msgstr "" -#: Shell/UsersShell.php:138;166 +#: src/Shell/UsersShell.php:140 +#: src/Shell/UsersShell.php:168 msgid "New password: {0}" msgstr "" -#: Shell/UsersShell.php:156;184;262;359 +#: src/Shell/UsersShell.php:158 +#: src/Shell/UsersShell.php:186 +#: src/Shell/UsersShell.php:214 +#: src/Shell/UsersShell.php:304 +#: src/Shell/UsersShell.php:403 msgid "Please enter a username." msgstr "" -#: Shell/UsersShell.php:165 +#: src/Shell/UsersShell.php:167 msgid "Password changed for user: {0}" msgstr "" -#: Shell/UsersShell.php:187 +#: src/Shell/UsersShell.php:189 msgid "Please enter a role." msgstr "" -#: Shell/UsersShell.php:193 +#: src/Shell/UsersShell.php:195 msgid "Role changed for user: {0}" msgstr "" -#: Shell/UsersShell.php:194 +#: src/Shell/UsersShell.php:196 msgid "New role: {0}" msgstr "" -#: Shell/UsersShell.php:209 +#: src/Shell/UsersShell.php:217 +msgid "Please enter a token." +msgstr "" + +#: src/Shell/UsersShell.php:224 +msgid "User was not saved, check validation errors" +msgstr "" + +#: src/Shell/UsersShell.php:229 +msgid "Api token changed for user: {0}" +msgstr "" + +#: src/Shell/UsersShell.php:230 +msgid "New token: {0}" +msgstr "" + +#: src/Shell/UsersShell.php:245 msgid "User was activated: {0}" msgstr "" -#: Shell/UsersShell.php:224 +#: src/Shell/UsersShell.php:260 msgid "User was de-activated: {0}" msgstr "" -#: Shell/UsersShell.php:236 +#: src/Shell/UsersShell.php:272 msgid "Please enter a username or email." msgstr "" -#: Shell/UsersShell.php:244 +#: src/Shell/UsersShell.php:280 msgid "Please ask the user to check the email to continue with password reset process" msgstr "" -#: Shell/UsersShell.php:308 +#: src/Shell/UsersShell.php:350 msgid "Superuser added:" msgstr "" -#: Shell/UsersShell.php:310 +#: src/Shell/UsersShell.php:352 msgid "User added:" msgstr "" -#: Shell/UsersShell.php:312 +#: src/Shell/UsersShell.php:354 msgid "Id: {0}" msgstr "" -#: Shell/UsersShell.php:313 +#: src/Shell/UsersShell.php:355 msgid "Username: {0}" msgstr "" -#: Shell/UsersShell.php:314 +#: src/Shell/UsersShell.php:356 msgid "Email: {0}" msgstr "" -#: Shell/UsersShell.php:315 +#: src/Shell/UsersShell.php:357 msgid "Role: {0}" msgstr "" -#: Shell/UsersShell.php:316 +#: src/Shell/UsersShell.php:358 msgid "Password: {0}" msgstr "" -#: Shell/UsersShell.php:318 +#: src/Shell/UsersShell.php:360 msgid "User could not be added:" msgstr "" -#: Shell/UsersShell.php:321 +#: src/Shell/UsersShell.php:363 msgid "Field: {0} Error: {1}" msgstr "" -#: Shell/UsersShell.php:337 +#: src/Shell/UsersShell.php:379 msgid "The user was not found." msgstr "" -#: Shell/UsersShell.php:367 +#: src/Shell/UsersShell.php:414 msgid "The user {0} was not deleted. Please try again" msgstr "" -#: Shell/UsersShell.php:369 +#: src/Shell/UsersShell.php:416 msgid "The user {0} was deleted successfully" msgstr "" -#: Template/Email/html/reset_password.ctp:21 -#: Template/Email/html/social_account_validation.ctp:14 -#: Template/Email/html/validation.ctp:21 -#: Template/Email/text/reset_password.ctp:20 -#: Template/Email/text/social_account_validation.ctp:22 -#: Template/Email/text/validation.ctp:20 -msgid "Hi {0}" -msgstr "" - -#: Template/Email/html/reset_password.ctp:24 -msgid "Reset your password here" -msgstr "" - -#: Template/Email/html/reset_password.ctp:27 -#: Template/Email/html/social_account_validation.ctp:32 -#: Template/Email/html/validation.ctp:27 -msgid "If the link is not correctly displayed, please copy the following address in your web browser {0}" +#: src/View/Helper/UserHelper.php:50 +msgid "Sign in with" msgstr "" -#: Template/Email/html/reset_password.ctp:34 -#: Template/Email/html/social_account_validation.ctp:39 -#: Template/Email/html/validation.ctp:34 -#: Template/Email/text/reset_password.ctp:28 -#: Template/Email/text/social_account_validation.ctp:30 -#: Template/Email/text/validation.ctp:28 -msgid "Thank you" +#: src/View/Helper/UserHelper.php:113 +msgid "Logout" msgstr "" -#: Template/Email/html/social_account_validation.ctp:18 -msgid "Activate your social login here" +#: src/View/Helper/UserHelper.php:134 +msgid "Welcome, {0}" msgstr "" -#: Template/Email/html/validation.ctp:24 -msgid "Activate your account here" +#: src/View/Helper/UserHelper.php:165 +msgid "reCaptcha is not configured! Please configure Users.reCaptcha.key" msgstr "" -#: Template/Email/text/reset_password.ctp:22 -#: Template/Email/text/validation.ctp:22 -msgid "Please copy the following address in your web browser {0}" +#: src/View/Helper/UserHelper.php:218 +msgid "Connected with {0}" msgstr "" -#: Template/Email/text/social_account_validation.ctp:24 -msgid "Please copy the following address in your web browser to activate your social login {0}" +#: src/View/Helper/UserHelper.php:223 +msgid "Connect with {0}" msgstr "" -#: Template/Users/add.ctp:13 -#: Template/Users/edit.ctp:17 -#: Template/Users/index.ctp:13;26 -#: Template/Users/view.ctp:15 +#: templates/Users/add.php:13 +#: templates/Users/edit.php:17 +#: templates/Users/index.php:13 +#: templates/Users/index.php:26 +#: templates/Users/view.php:15 msgid "Actions" msgstr "" -#: Template/Users/add.ctp:15 -#: Template/Users/edit.ctp:28 -#: Template/Users/view.ctp:23 +#: templates/Users/add.php:15 +#: templates/Users/edit.php:28 +#: templates/Users/view.php:23 msgid "List Users" msgstr "" -#: Template/Users/add.ctp:21 -#: Template/Users/register.ctp:18 +#: templates/Users/add.php:21 +#: templates/Users/register.php:18 msgid "Add User" msgstr "" -#: Template/Users/add.ctp:23 -#: Template/Users/edit.ctp:36 -#: Template/Users/index.ctp:22 -#: Template/Users/login.ctp:20 -#: Template/Users/profile.ctp:30 -#: Template/Users/register.ctp:20 -#: Template/Users/view.ctp:33 +#: templates/Users/add.php:23 +#: templates/Users/edit.php:36 +#: templates/Users/index.php:22 +#: templates/Users/login.php:20 +#: templates/Users/profile.php:30 +#: templates/Users/register.php:20 +#: templates/Users/view.php:33 msgid "Username" msgstr "" -#: Template/Users/add.ctp:24 -#: Template/Users/edit.ctp:37 -#: Template/Users/index.ctp:23 -#: Template/Users/profile.ctp:32 -#: Template/Users/register.ctp:21 -#: Template/Users/view.ctp:35 +#: templates/Users/add.php:24 +#: templates/Users/edit.php:37 +#: templates/Users/index.php:23 +#: templates/Users/profile.php:32 +#: templates/Users/register.php:21 +#: templates/Users/view.php:35 msgid "Email" msgstr "" -#: Template/Users/add.ctp:25 -#: Template/Users/login.ctp:21 -#: Template/Users/register.ctp:22 +#: templates/Users/add.php:25 +#: templates/Users/login.php:21 +#: templates/Users/register.php:22 msgid "Password" msgstr "" -#: Template/Users/add.ctp:26 -#: Template/Users/edit.ctp:38 -#: Template/Users/index.ctp:24 -#: Template/Users/register.ctp:27 +#: templates/Users/add.php:26 +#: templates/Users/edit.php:38 +#: templates/Users/index.php:24 +#: templates/Users/register.php:28 msgid "First name" msgstr "" -#: Template/Users/add.ctp:27 -#: Template/Users/edit.ctp:39 -#: Template/Users/index.ctp:25 -#: Template/Users/register.ctp:28 +#: templates/Users/add.php:27 +#: templates/Users/edit.php:39 +#: templates/Users/index.php:25 +#: templates/Users/register.php:29 msgid "Last name" msgstr "" -#: Template/Users/add.ctp:30 -#: Template/Users/edit.ctp:54 -#: Template/Users/view.ctp:49;74 +#: templates/Users/add.php:30 +#: templates/Users/edit.php:54 +#: templates/Users/view.php:49 +#: templates/Users/view.php:74 msgid "Active" msgstr "" -#: Template/Users/add.ctp:34 -#: Template/Users/change_password.ctp:25 -#: Template/Users/edit.ctp:58 -#: Template/Users/register.ctp:37 -#: Template/Users/request_reset_password.ctp:8 -#: Template/Users/resend_token_validation.ctp:20 -#: Template/Users/social_email.ctp:19 +#: templates/Users/add.php:34 +#: templates/Users/change_password.php:25 +#: templates/Users/edit.php:58 +#: templates/Users/register.php:38 +#: templates/Users/request_reset_password.php:23 +#: templates/Users/resend_token_validation.php:20 +#: templates/Users/social_email.php:19 msgid "Submit" msgstr "" -#: Template/Users/change_password.ctp:5 +#: templates/Users/change_password.php:5 msgid "Please enter the new password" msgstr "" -#: Template/Users/change_password.ctp:10 +#: templates/Users/change_password.php:10 msgid "Current password" msgstr "" -#: Template/Users/change_password.ctp:16 +#: templates/Users/change_password.php:16 msgid "New password" msgstr "" -#: Template/Users/change_password.ctp:21 -#: Template/Users/register.ctp:25 +#: templates/Users/change_password.php:21 +#: templates/Users/register.php:26 msgid "Confirm password" msgstr "" -#: Template/Users/edit.ctp:22 -#: Template/Users/index.ctp:40 +#: templates/Users/edit.php:22 +#: templates/Users/index.php:40 msgid "Delete" msgstr "" -#: Template/Users/edit.ctp:24 -#: Template/Users/index.ctp:40 -#: Template/Users/view.ctp:21 +#: templates/Users/edit.php:24 +#: templates/Users/index.php:40 +#: templates/Users/view.php:21 msgid "Are you sure you want to delete # {0}?" msgstr "" -#: Template/Users/edit.ctp:34 -#: Template/Users/view.ctp:17 +#: templates/Users/edit.php:34 +#: templates/Users/view.php:17 msgid "Edit User" msgstr "" -#: Template/Users/edit.ctp:40 -#: Template/Users/view.ctp:43 +#: templates/Users/edit.php:40 +#: templates/Users/view.php:43 msgid "Token" msgstr "" -#: Template/Users/edit.ctp:42 +#: templates/Users/edit.php:42 msgid "Token expires" msgstr "" -#: Template/Users/edit.ctp:45 +#: templates/Users/edit.php:45 msgid "API token" msgstr "" -#: Template/Users/edit.ctp:48 +#: templates/Users/edit.php:48 msgid "Activation date" msgstr "" -#: Template/Users/edit.ctp:51 +#: templates/Users/edit.php:51 msgid "TOS date" msgstr "" -#: Template/Users/edit.ctp:64 +#: templates/Users/edit.php:64 msgid "Reset Google Authenticator Token" msgstr "" -#: Template/Users/edit.ctp:70 +#: templates/Users/edit.php:70 msgid "Are you sure you want to reset token for user \"{0}\"?" msgstr "" -#: Template/Users/index.ctp:15 +#: templates/Users/index.php:15 msgid "New {0}" msgstr "" -#: Template/Users/index.ctp:37 +#: templates/Users/index.php:37 msgid "View" msgstr "" -#: Template/Users/index.ctp:38 +#: templates/Users/index.php:38 msgid "Change password" msgstr "" -#: Template/Users/index.ctp:39 +#: templates/Users/index.php:39 msgid "Edit" msgstr "" -#: Template/Users/index.ctp:49 +#: templates/Users/index.php:49 msgid "previous" msgstr "" -#: Template/Users/index.ctp:51 +#: templates/Users/index.php:51 msgid "next" msgstr "" -#: Template/Users/login.ctp:19 +#: templates/Users/login.php:19 msgid "Please enter your username and password" msgstr "" -#: Template/Users/login.ctp:29 +#: templates/Users/login.php:29 msgid "Remember me" msgstr "" -#: Template/Users/login.ctp:37 +#: templates/Users/login.php:37 msgid "Register" msgstr "" -#: Template/Users/login.ctp:43 +#: templates/Users/login.php:43 msgid "Reset Password" msgstr "" -#: Template/Users/login.ctp:48 +#: templates/Users/login.php:48 msgid "Login" msgstr "" -#: Template/Users/profile.ctp:21 +#: templates/Users/profile.php:21 msgid "{0} {1}" msgstr "" -#: Template/Users/profile.ctp:27 +#: templates/Users/profile.php:27 msgid "Change Password" msgstr "" -#: Template/Users/profile.ctp:38 -#: Template/Users/view.ctp:68 +#: templates/Users/profile.php:38 +#: templates/Users/view.php:68 msgid "Social Accounts" msgstr "" -#: Template/Users/profile.ctp:42 -#: Template/Users/view.ctp:73 +#: templates/Users/profile.php:42 +#: templates/Users/view.php:73 msgid "Avatar" msgstr "" -#: Template/Users/profile.ctp:43 -#: Template/Users/view.ctp:72 +#: templates/Users/profile.php:43 +#: templates/Users/view.php:72 msgid "Provider" msgstr "" -#: Template/Users/profile.ctp:44 +#: templates/Users/profile.php:44 msgid "Link" msgstr "" -#: Template/Users/profile.ctp:51 +#: templates/Users/profile.php:51 msgid "Link to {0}" msgstr "" -#: Template/Users/register.ctp:30 +#: templates/Users/register.php:31 msgid "Accept TOS conditions?" msgstr "" -#: Template/Users/request_reset_password.ctp:5 -msgid "Please enter your email to reset your password" +#: templates/Users/request_reset_password.php:20 +msgid "Please enter your email or username to reset your password" msgstr "" -#: Template/Users/resend_token_validation.ctp:15 +#: templates/Users/resend_token_validation.php:15 msgid "Resend Validation email" msgstr "" -#: Template/Users/resend_token_validation.ctp:17 +#: templates/Users/resend_token_validation.php:17 msgid "Email or username" msgstr "" -#: Template/Users/verify.ctp:13 +#: templates/Users/u2f_authenticate.php:22 +#: templates/Users/webauthn2fa.php:25 +msgid "Verify your registered yubico key" +msgstr "" + +#: templates/Users/u2f_authenticate.php:23 +#: templates/Users/u2f_register.php:23 +#: templates/Users/webauthn2fa.php:20 +#: templates/Users/webauthn2fa.php:26 +msgid "Please insert and tap your yubico key" +msgstr "" + +#: templates/Users/u2f_authenticate.php:24 +#: templates/Users/webauthn2fa.php:27 +msgid "You can now finish the authentication process using the registered device." +msgstr "" + +#: templates/Users/u2f_authenticate.php:25 +#: templates/Users/webauthn2fa.php:22 +#: templates/Users/webauthn2fa.php:28 +msgid "When the YubiKey starts blinking, press the golden disc to activate it. Depending on the web browser you might need to confirm the use of extended information from the YubiKey." +msgstr "" + +#: templates/Users/u2f_authenticate.php:27 +#: templates/Users/u2f_register.php:27 +#: templates/Users/webauthn2fa.php:31 +msgid "Reload" +msgstr "" + +#: templates/Users/u2f_authenticate.php:51 +#: templates/Users/u2f_register.php:56 +msgid "Yubico key check has failed, please try again" +msgstr "" + +#: templates/Users/u2f_register.php:22 +#: templates/Users/webauthn2fa.php:19 +msgid "Registering your yubico key" +msgstr "" + +#: templates/Users/verify.php:13 msgid "Verification Code" msgstr "" -#: Template/Users/verify.ctp:15 +#: templates/Users/verify.php:15 msgid " Verify" msgstr "" -#: Template/Users/view.ctp:19 +#: templates/Users/view.php:19 msgid "Delete User" msgstr "" -#: Template/Users/view.ctp:24 +#: templates/Users/view.php:24 msgid "New User" msgstr "" -#: Template/Users/view.ctp:31 +#: templates/Users/view.php:31 msgid "Id" msgstr "" -#: Template/Users/view.ctp:37 +#: templates/Users/view.php:37 msgid "First Name" msgstr "" -#: Template/Users/view.ctp:39 +#: templates/Users/view.php:39 msgid "Last Name" msgstr "" -#: Template/Users/view.ctp:41 +#: templates/Users/view.php:41 msgid "Role" msgstr "" -#: Template/Users/view.ctp:45 +#: templates/Users/view.php:45 msgid "Api Token" msgstr "" -#: Template/Users/view.ctp:53 +#: templates/Users/view.php:53 msgid "Token Expires" msgstr "" -#: Template/Users/view.ctp:55 +#: templates/Users/view.php:55 msgid "Activation Date" msgstr "" -#: Template/Users/view.ctp:57 +#: templates/Users/view.php:57 msgid "Tos Date" msgstr "" -#: Template/Users/view.ctp:59;75 +#: templates/Users/view.php:59 +#: templates/Users/view.php:75 msgid "Created" msgstr "" -#: Template/Users/view.ctp:61;76 +#: templates/Users/view.php:61 +#: templates/Users/view.php:76 msgid "Modified" msgstr "" -#: View/Helper/UserHelper.php:46 -msgid "Sign in with" +#: templates/Users/webauthn2fa.php:8 +msgid "Two-factor authentication" msgstr "" -#: View/Helper/UserHelper.php:103 -msgid "Logout" +#: templates/Users/webauthn2fa.php:21 +msgid "In order to enable your YubiKey the first step is to perform a registration." msgstr "" -#: View/Helper/UserHelper.php:121 -msgid "Welcome, {0}" +#: templates/email/html/reset_password.php:14 +#: templates/email/html/social_account_validation.php:14 +#: templates/email/html/validation.php:13 +#: templates/email/text/reset_password.php:12 +#: templates/email/text/social_account_validation.php:12 +#: templates/email/text/validation.php:12 +msgid "Hi {0}" msgstr "" -#: View/Helper/UserHelper.php:151 -msgid "reCaptcha is not configured! Please configure Users.reCaptcha.key" +#: templates/email/html/reset_password.php:17 +msgid "Reset your password here" msgstr "" -#: View/Helper/UserHelper.php:215 -msgid "Connected with {0}" +#: templates/email/html/reset_password.php:20 +#: templates/email/html/social_account_validation.php:23 +#: templates/email/html/validation.php:19 +msgid "If the link is not correctly displayed, please copy the following address in your web browser {0}" msgstr "" -#: View/Helper/UserHelper.php:220 -msgid "Connect with {0}" +#: templates/email/html/reset_password.php:27 +#: templates/email/html/social_account_validation.php:30 +#: templates/email/html/validation.php:26 +#: templates/email/text/reset_password.php:20 +#: templates/email/text/social_account_validation.php:20 +#: templates/email/text/validation.php:20 +msgid "Thank you" +msgstr "" + +#: templates/email/html/social_account_validation.php:18 +msgid "Activate your social login here" +msgstr "" + +#: templates/email/html/validation.php:16 +msgid "Activate your account here" +msgstr "" + +#: templates/email/text/reset_password.php:14 +#: templates/email/text/validation.php:14 +msgid "Please copy the following address in your web browser {0}" +msgstr "" + +#: templates/email/text/social_account_validation.php:14 +msgid "Please copy the following address in your web browser to activate your social login {0}" msgstr "" diff --git a/src/Authenticator/SocialAuthTrait.php b/src/Authenticator/SocialAuthTrait.php index 9a0ff5b6a..9f3cef77c 100644 --- a/src/Authenticator/SocialAuthTrait.php +++ b/src/Authenticator/SocialAuthTrait.php @@ -24,7 +24,6 @@ trait SocialAuthTrait { /** * @param array $rawData social user raw data - * * @return \Authentication\Authenticator\Result */ protected function identify($rawData) @@ -41,7 +40,7 @@ protected function identify($rawData) } catch (UserNotActiveException $e) { return new Result(null, self::FAILURE_USER_NOT_ACTIVE); } catch (MissingEmailException $exception) { - throw new SocialAuthenticationException(compact('rawData'), null, $exception); + throw new SocialAuthenticationException(['rawData' => $rawData], null, $exception); } } } diff --git a/src/Controller/AppController.php b/src/Controller/AppController.php index 9ede33800..68c4951e2 100644 --- a/src/Controller/AppController.php +++ b/src/Controller/AppController.php @@ -17,7 +17,6 @@ /** * AppController for Users Plugin - * */ class AppController extends BaseController { diff --git a/src/Controller/Component/LoginComponent.php b/src/Controller/Component/LoginComponent.php index 5db67b8d0..7ddbbf420 100644 --- a/src/Controller/Component/LoginComponent.php +++ b/src/Controller/Component/LoginComponent.php @@ -17,10 +17,12 @@ use Cake\Controller\Component; use Cake\Core\Configure; use Cake\Http\ServerRequest; +use Cake\Log\Log; use CakeDC\Auth\Authentication\AuthenticationService; use CakeDC\Auth\Traits\IsAuthorizedTrait; use CakeDC\Users\Plugin; use CakeDC\Users\Utility\UsersUrl; +use Laminas\Diactoros\Uri; /** * LoginFailure component @@ -68,6 +70,7 @@ public function handleLogin($errorOnlyPost, $redirectFailure) if ($result->isValid()) { $user = $request->getAttribute('identity')->getOriginalData(); $this->handlePasswordRehash($service, $user, $request); + $this->updateLastLogin($user); return $this->afterIdentifyUser($user); } @@ -82,7 +85,6 @@ public function handleLogin($errorOnlyPost, $redirectFailure) * Handle login failure * * @param bool $redirect should redirect? - * * @return \Cake\Http\Response|null */ public function handleFailure($redirect = true) @@ -147,13 +149,24 @@ protected function afterIdentifyUser($user) { $event = $this->getController()->dispatchEvent(Plugin::EVENT_AFTER_LOGIN, ['user' => $user]); if (is_array($event->getResult())) { + // in this case we don't checkSafeHost the url as the url params are generated by an event return $this->getController()->redirect($event->getResult()); } - $query = $this->getController()->getRequest()->getQueryParams(); + $queryRedirect = $this->getController()->getRequest()->getQuery('redirect'); $redirectUrl = $this->getController()->Authentication->getConfig('loginRedirect'); - if ($this->isAuthorized($query['redirect'] ?? null)) { - $redirectUrl = $query['redirect']; + if (!$this->checkSafeHost($queryRedirect)) { + $userId = $user['id'] ?? null; + Log::info( + "Unsafe redirect `$queryRedirect` ignored, user id `{$userId}` " . + "redirected to `$redirectUrl` after successful login" + ); + $queryRedirect = $redirectUrl; + } + // even if the host is safe, we need to check if the url is authorized for the given user + // this check ignores the host + if ($this->isAuthorized($queryRedirect ?? null)) { + $redirectUrl = $queryRedirect; } return $this->getController()->redirect($redirectUrl); @@ -165,7 +178,6 @@ protected function afterIdentifyUser($user) * @param \CakeDC\Auth\Authentication\AuthenticationService $service Authentication service * @param \CakeDC\Users\Model\Entity\User $user User entity. * @param \Cake\Http\ServerRequest $request The http request. - * * @return void */ protected function handlePasswordRehash($service, $user, \Cake\Http\ServerRequest $request) @@ -186,4 +198,41 @@ protected function handlePasswordRehash($service, $user, \Cake\Http\ServerReques break; } } + + /** + * Check if there is a host defined in the $queryRedirect and it's in the allowed list of hosts + * + * @param string|null $queryRedirect redirect url + * @return bool + */ + protected function checkSafeHost(?string $queryRedirect = null): bool + { + if ($queryRedirect === null) { + return true; + } + + $uri = new Uri($queryRedirect); + $host = $uri->getHost(); + if (!$host) { + return true; + } + + return in_array($host, Configure::read('Users.AllowedRedirectHosts')); + } + + /** + * Update last loging date + * + * @param \CakeDC\Users\Model\Entity\User $user User entity. + * @return void + */ + protected function updateLastLogin($user) + { + $now = \Cake\I18n\FrozenTime::now(); + $user->set('last_login', $now); + $this->getController()->getUsersTable()->updateAll( + ['last_login' => $now], + ['id' => $user->id] + ); + } } diff --git a/src/Controller/Traits/CustomUsersTableTrait.php b/src/Controller/Traits/CustomUsersTableTrait.php index 03d6c304e..c003996ca 100644 --- a/src/Controller/Traits/CustomUsersTableTrait.php +++ b/src/Controller/Traits/CustomUsersTableTrait.php @@ -19,7 +19,6 @@ /** * Customize Users Table - * */ trait CustomUsersTableTrait { diff --git a/src/Controller/Traits/LinkSocialTrait.php b/src/Controller/Traits/LinkSocialTrait.php index c706d2d25..a2fcf802f 100644 --- a/src/Controller/Traits/LinkSocialTrait.php +++ b/src/Controller/Traits/LinkSocialTrait.php @@ -19,7 +19,6 @@ /** * Actions to allow user to link social accounts - * */ trait LinkSocialTrait { @@ -27,7 +26,6 @@ trait LinkSocialTrait * Init link and auth process against provider * * @param string $alias of the provider. - * * @throws \Cake\Http\Exception\NotFoundException Quando o provider informado não existe * @return \Cake\Http\Response Redirects on successful */ @@ -50,7 +48,6 @@ public function linkSocial($alias = null) * Callback to get user information from provider * * @param string $alias of the provider. - * * @throws \Cake\Http\Exception\NotFoundException Quando o provider informado não existe * @return \Cake\Http\Response Redirects to profile if okay or error */ @@ -84,7 +81,7 @@ public function callbackLinkSocial($alias = null) } } catch (\Exception $e) { $log = sprintf( - "Error linking social account: %s %s", + 'Error linking social account: %s %s', $e->getMessage(), $e ); diff --git a/src/Controller/Traits/OneTimePasswordVerifyTrait.php b/src/Controller/Traits/OneTimePasswordVerifyTrait.php index ea827aa63..9538569a6 100644 --- a/src/Controller/Traits/OneTimePasswordVerifyTrait.php +++ b/src/Controller/Traits/OneTimePasswordVerifyTrait.php @@ -55,7 +55,7 @@ public function verify() $temporarySession['email'], $secret ); - $this->set(compact('secretDataUri')); + $this->set(['secretDataUri' => $secretDataUri]); } if ($this->getRequest()->is('post')) { @@ -104,7 +104,6 @@ protected function isVerifyAllowed() * Get the Google Authenticator secret of user, if not exists try to create one and save * * @param \CakeDC\Users\Model\Entity\User $user user data present on session - * * @return string if empty the creation has failed */ protected function onVerifyGetSecret($user) @@ -144,7 +143,6 @@ protected function onVerifyGetSecret($user) * Handle the action when user post the form with code * * @param array $loginAction url to login page used in redirect - * * @return \Cake\Http\Response */ protected function onPostVerifyCode($loginAction) @@ -178,7 +176,6 @@ protected function onPostVerifyCode($loginAction) * * @param array $loginAction url to login page used in redirect * @param \CakeDC\Users\Model\Entity\User $user user data present on session - * * @return \Cake\Http\Response */ protected function onPostVerifyCodeOkay($loginAction, $user) diff --git a/src/Controller/Traits/PasswordManagementTrait.php b/src/Controller/Traits/PasswordManagementTrait.php index 1dd7de656..09878ae2a 100644 --- a/src/Controller/Traits/PasswordManagementTrait.php +++ b/src/Controller/Traits/PasswordManagementTrait.php @@ -36,12 +36,13 @@ trait PasswordManagementTrait * reset password with session key (email token has already been validated) * * @param int|string|null $id user_id, null for logged in user id - * * @return mixed */ public function changePassword($id = null) { $user = $this->getUsersTable()->newEntity([], ['validate' => false]); + $user->setNew(false); + $identity = $this->getRequest()->getAttribute('identity'); $identity = $identity ?? []; $userId = $identity['id'] ?? null; @@ -59,7 +60,7 @@ public function changePassword($id = null) $redirect = Configure::read('Users.Profile.route'); } else { $this->Flash->error( - __d('CakeDC/Users', 'Changing another user\'s password is not allowed') + __d('cake_d_c/users', 'Changing another user\'s password is not allowed') ); $this->redirect(Configure::read('Users.Profile.route')); @@ -86,11 +87,12 @@ public function changePassword($id = null) if ($validatePassword) { $validator = $this->getUsersTable()->validationCurrentPassword($validator); } + $this->getUsersTable()->setValidator('current', $validator); $user = $this->getUsersTable()->patchEntity( $user, $this->getRequest()->getData(), [ - 'validate' => $validator, + 'validate' => 'current', 'accessibleFields' => [ 'current_password' => true, 'password' => true, @@ -124,7 +126,7 @@ public function changePassword($id = null) $this->log($exception->getMessage()); } } - $this->set(compact('user')); + $this->set(['user' => $user]); $this->set('_serialize', ['user']); } diff --git a/src/Controller/Traits/ProfileTrait.php b/src/Controller/Traits/ProfileTrait.php index 1de0b787e..a67ac991c 100644 --- a/src/Controller/Traits/ProfileTrait.php +++ b/src/Controller/Traits/ProfileTrait.php @@ -27,6 +27,7 @@ trait ProfileTrait { /** * Profile action + * * @param mixed $id Profile id object. * @return mixed */ @@ -58,7 +59,7 @@ public function profile($id = null) return $this->redirect($this->getRequest()->referer()); } - $this->set(compact('user', 'isCurrentUser')); + $this->set(['user' => $user, 'isCurrentUser' => $isCurrentUser]); $this->set('_serialize', ['user', 'isCurrentUser']); } } diff --git a/src/Controller/Traits/ReCaptchaTrait.php b/src/Controller/Traits/ReCaptchaTrait.php index fb9ab7bcc..410511b6e 100644 --- a/src/Controller/Traits/ReCaptchaTrait.php +++ b/src/Controller/Traits/ReCaptchaTrait.php @@ -44,7 +44,7 @@ public function validateReCaptcha($recaptchaResponse, $clientIp) /** * Create reCaptcha instance if enabled in configuration * - * @return \ReCaptcha\ReCaptcha + * @return \ReCaptcha\ReCaptcha|null */ protected function _getReCaptchaInstance() { diff --git a/src/Controller/Traits/RegisterTrait.php b/src/Controller/Traits/RegisterTrait.php index f48c553cf..af9d6d0f7 100644 --- a/src/Controller/Traits/RegisterTrait.php +++ b/src/Controller/Traits/RegisterTrait.php @@ -31,8 +31,8 @@ trait RegisterTrait /** * Register a new user * - * @throws \Cake\Http\Exception\NotFoundException * @return mixed + * @throws \Cake\Http\Exception\NotFoundException */ public function register() { @@ -50,7 +50,7 @@ public function register() } $usersTable = $this->getUsersTable(); - $user = $usersTable->newEntity([]); + $user = $usersTable->newEmptyEntity(); $validateEmail = (bool)Configure::read('Users.Email.validate'); $useTos = (bool)Configure::read('Users.Tos.required'); $tokenExpiration = Configure::read('Users.Token.expiration'); @@ -71,10 +71,18 @@ public function register() $data = $result->toArray(); $data['password'] = $requestData['password'] ?? null; //since password is a hidden property $userSaved = $usersTable->register($user, $data, $options); + $errors = \collection($user->getErrors())->unfold()->toArray(); if ($userSaved) { return $this->_afterRegister($userSaved); - } else { + } elseif (Configure::read('Users.Registration.showVerboseError') && count($errors) > 0) { $this->set(compact('user')); + foreach ($errors as $error) { + $this->Flash->error(__($error)); + } + + return; + } else { + $this->set(['user' => $user]); $this->Flash->error(__d('cake_d_c/users', 'The user could not be saved')); return; @@ -84,7 +92,7 @@ public function register() return $this->redirect($event->getResult()); } - $this->set(compact('user')); + $this->set(['user' => $user]); $this->set('_serialize', ['user']); if (!$this->getRequest()->is('post')) { @@ -98,7 +106,14 @@ public function register() } $userSaved = $usersTable->register($user, $requestData, $options); - if (!$userSaved) { + $errors = \collection($user->getErrors())->unfold()->toArray(); + if (!$userSaved && Configure::read('Users.Registration.showVerboseError') && count($errors) > 0) { + foreach ($errors as $error) { + $this->Flash->error(__($error)); + } + + return; + } elseif (!$userSaved) { $this->Flash->error(__d('cake_d_c/users', 'The user could not be saved')); return; diff --git a/src/Controller/Traits/SimpleCrudTrait.php b/src/Controller/Traits/SimpleCrudTrait.php index fb49432d5..33346d298 100644 --- a/src/Controller/Traits/SimpleCrudTrait.php +++ b/src/Controller/Traits/SimpleCrudTrait.php @@ -64,7 +64,7 @@ public function add() { $table = $this->loadModel(); $tableAlias = $table->getAlias(); - $entity = $table->newEntity([]); + $entity = $table->newEmptyEntity(); $this->set($tableAlias, $entity); $this->set('tableAlias', $tableAlias); $this->set('_serialize', [$tableAlias, 'tableAlias']); diff --git a/src/Controller/Traits/U2fTrait.php b/src/Controller/Traits/U2fTrait.php index 90f4151a4..a469a9ce8 100644 --- a/src/Controller/Traits/U2fTrait.php +++ b/src/Controller/Traits/U2fTrait.php @@ -30,7 +30,6 @@ trait U2fTrait * Perform redirect keeping current query string * * @param array $url base url - * * @return \Cake\Http\Response */ public function redirectWithQuery($url) @@ -56,7 +55,6 @@ public function u2f() 'action' => 'login', ]); } - if (!$data['registration']) { return $this->redirectWithQuery([ 'action' => 'u2fRegister', @@ -86,7 +84,7 @@ public function u2fRegister() if (!$data['registration']) { [$registerRequest, $signs] = $this->createU2fLib()->getRegisterData(); $this->getRequest()->getSession()->write('U2f.registerRequest', json_encode($registerRequest)); - $this->set(compact('registerRequest', 'signs')); + $this->set(['registerRequest' => $registerRequest, 'signs' => $signs]); return null; } @@ -147,7 +145,7 @@ public function u2fAuthenticate() } $authenticateRequest = $this->createU2fLib()->getAuthenticateData([$data['registration']]); $this->getRequest()->getSession()->write('U2f.authenticateRequest', json_encode($authenticateRequest)); - $this->set(compact('authenticateRequest')); + $this->set(['authenticateRequest' => $authenticateRequest]); return null; } diff --git a/src/Controller/Traits/UserValidationTrait.php b/src/Controller/Traits/UserValidationTrait.php index d746a4eb4..16bcf90a0 100644 --- a/src/Controller/Traits/UserValidationTrait.php +++ b/src/Controller/Traits/UserValidationTrait.php @@ -42,6 +42,10 @@ public function validate($type = null, $token = null) try { $result = $this->getUsersTable()->validate($token, 'activateUser'); if ($result) { + $event = $this->dispatchEvent(Plugin::EVENT_AFTER_EMAIL_TOKEN_VALIDATION, ['user' => $result]); + if (!empty($event) && is_array($event->getResult())) { + return $this->redirect($event->getResult()); + } $this->Flash->success(__d('cake_d_c/users', 'User account validated successfully')); } else { $this->Flash->error(__d('cake_d_c/users', 'User account could not be validated')); diff --git a/src/Controller/Traits/Webauthn2faTrait.php b/src/Controller/Traits/Webauthn2faTrait.php new file mode 100644 index 000000000..034a17442 --- /dev/null +++ b/src/Controller/Traits/Webauthn2faTrait.php @@ -0,0 +1,142 @@ +getWebauthn2faRegisterAdapter(); + $user = $adapter->getUser(); + $this->set('isRegister', !$adapter->hasCredential()); + $this->set('username', $user->webauthn_username ?? $user->username); + } + + /** + * Action to provide register options to frontend (from js requests) + * + * @return \Cake\Http\Response + */ + public function webauthn2faRegisterOptions() + { + $adapter = $this->getWebauthn2faRegisterAdapter(); + if (!$adapter->hasCredential()) { + return $this->getResponse() + ->withStringBody(json_encode($adapter->getOptions())); + } + + throw new BadRequestException( + __d('cake_d_c/users', 'User already has configured webauthn2fa') + ); + } + + /** + * Action to verify and save the new credential based on the webauthn register response. + * + * @return \Cake\Http\Response + * @throws \Throwable + */ + public function webauthn2faRegister(): \Cake\Http\Response + { + try { + $adapter = $this->getWebauthn2faRegisterAdapter(); + if (!$adapter->hasCredential()) { + $adapter->verifyResponse(); + + return $this->getResponse()->withStringBody(json_encode(['success' => true])); + } + throw new BadRequestException( + __d('cake_d_c/users', 'User already has configured webauthn2fa') + ); + } catch (\Throwable $e) { + $user = $this->request->getSession()->read('Webauthn2fa.User'); + Log::debug(__d('cake_d_c/users', 'Register error with webauthn for user id: {0}', $user['id'] ?? 'empty')); + throw $e; + } + } + + /** + * Action to provide authenticate options to frontend (from js requests) + * + * @return \Cake\Http\Response + */ + public function webauthn2faAuthenticateOptions(): \Cake\Http\Response + { + $adapter = $this->getWebauthn2faAuthenticateAdapter(); + + return $this->getResponse()->withStringBody( + json_encode($adapter->getOptions()) + ); + } + + /** + * Action to authenticate user based on the webauthn authenticate response. + * + * @return \Cake\Http\Response + * @throws \Throwable + */ + public function webauthn2faAuthenticate(): \Cake\Http\Response + { + try { + $adapter = $this->getWebauthn2faAuthenticateAdapter(); + $adapter->verifyResponse(); + $redirectUrl = Configure::read('Auth.AuthenticationComponent.loginAction') + [ + '?' => $this->getRequest()->getQueryParams(), + ]; + $this->getRequest()->getSession()->delete('Webauthn2fa'); + $this->getRequest()->getSession()->write( + TwoFactorAuthenticator::USER_SESSION_KEY, + $adapter->getUser() + ); + + return $this->getResponse()->withStringBody(json_encode([ + 'success' => true, + 'redirectUrl' => Router::url($redirectUrl), + ])); + } catch (\Throwable $e) { + $user = $this->request->getSession()->read('Webauthn2fa.User'); + Log::debug(__d('cake_d_c/users', 'Register error with webauthn for user id: {0}', $user['id'] ?? 'empty')); + throw $e; + } + } + + /** + * @return \CakeDC\Users\Webauthn\RegisterAdapter + */ + protected function getWebauthn2faRegisterAdapter(): RegisterAdapter + { + return new RegisterAdapter($this->getRequest(), $this->getUsersTable()); + } + + /** + * @return \CakeDC\Users\Webauthn\AuthenticateAdapter + */ + protected function getWebauthn2faAuthenticateAdapter(): AuthenticateAdapter + { + return new AuthenticateAdapter($this->getRequest()); + } +} diff --git a/src/Controller/UsersController.php b/src/Controller/UsersController.php index 741f06bf6..096bbc9c1 100644 --- a/src/Controller/UsersController.php +++ b/src/Controller/UsersController.php @@ -22,11 +22,13 @@ use CakeDC\Users\Controller\Traits\SimpleCrudTrait; use CakeDC\Users\Controller\Traits\SocialTrait; use CakeDC\Users\Controller\Traits\U2fTrait; +use CakeDC\Users\Controller\Traits\Webauthn2faTrait; /** * Users Controller * * @property \CakeDC\Users\Model\Table\UsersTable $Users + * @property \Cake\Controller\Component\SecurityComponent $Security */ class UsersController extends AppController { @@ -39,6 +41,7 @@ class UsersController extends AppController use SimpleCrudTrait; use SocialTrait; use U2fTrait; + use Webauthn2faTrait; /** * Initialize @@ -51,7 +54,17 @@ public function initialize(): void if ($this->components()->has('Security')) { $this->Security->setConfig( 'unlockedActions', - ['login', 'u2fRegister', 'u2fRegisterFinish', 'u2fAuthenticate', 'u2fAuthenticateFinish'] + [ + 'login', + 'u2fRegister', + 'u2fRegisterFinish', + 'u2fAuthenticate', + 'u2fAuthenticateFinish', + 'webauthn2faRegister', + 'webauthn2faRegisterOptions', + 'webauthn2faAuthenticate', + 'webauthn2faAuthenticateOptions', + ] ); } } diff --git a/src/Exception/AccountAlreadyActiveException.php b/src/Exception/AccountAlreadyActiveException.php index 879db6a7d..4cef96afd 100644 --- a/src/Exception/AccountAlreadyActiveException.php +++ b/src/Exception/AccountAlreadyActiveException.php @@ -13,12 +13,11 @@ namespace CakeDC\Users\Exception; -use Cake\Core\Exception\Exception; - -class AccountAlreadyActiveException extends Exception +class AccountAlreadyActiveException extends \Cake\Core\Exception\CakeException { /** * AccountAlreadyActiveException constructor. + * * @param array|string $message message * @param int $code code * @param null $previous previous diff --git a/src/Exception/AccountNotActiveException.php b/src/Exception/AccountNotActiveException.php index 1b8321cd7..a7d1f3741 100644 --- a/src/Exception/AccountNotActiveException.php +++ b/src/Exception/AccountNotActiveException.php @@ -13,14 +13,13 @@ namespace CakeDC\Users\Exception; -use Cake\Core\Exception\Exception; - -class AccountNotActiveException extends Exception +class AccountNotActiveException extends \Cake\Core\Exception\CakeException { protected $_messageTemplate = '/a/validate/%s/%s'; /** * AccountNotActiveException constructor. + * * @param array|string $message message * @param int $code code * @param null $previous previous diff --git a/src/Exception/MissingEmailException.php b/src/Exception/MissingEmailException.php index 6faf65205..6ada67bc6 100644 --- a/src/Exception/MissingEmailException.php +++ b/src/Exception/MissingEmailException.php @@ -13,12 +13,11 @@ namespace CakeDC\Users\Exception; -use Cake\Core\Exception\Exception; - -class MissingEmailException extends Exception +class MissingEmailException extends \Cake\Core\Exception\CakeException { /** * MissingEmailException constructor. + * * @param array|string $message message * @param int $code code * @param null $previous previous diff --git a/src/Exception/SocialAuthenticationException.php b/src/Exception/SocialAuthenticationException.php index ae6225081..bb9e03bf5 100644 --- a/src/Exception/SocialAuthenticationException.php +++ b/src/Exception/SocialAuthenticationException.php @@ -12,9 +12,7 @@ */ namespace CakeDC\Users\Exception; -use Cake\Core\Exception\Exception; - -class SocialAuthenticationException extends Exception +class SocialAuthenticationException extends \Cake\Core\Exception\CakeException { protected $_messageTemplate = 'Could not autheticate user'; protected $_defaultCode = 400; diff --git a/src/Exception/TokenExpiredException.php b/src/Exception/TokenExpiredException.php index eeb3fc974..ee036ecac 100644 --- a/src/Exception/TokenExpiredException.php +++ b/src/Exception/TokenExpiredException.php @@ -13,12 +13,11 @@ namespace CakeDC\Users\Exception; -use Cake\Core\Exception\Exception; - -class TokenExpiredException extends Exception +class TokenExpiredException extends \Cake\Core\Exception\CakeException { /** * TokenExpiredException constructor. + * * @param array|string $message message * @param int $code code * @param null $previous previous diff --git a/src/Exception/UserAlreadyActiveException.php b/src/Exception/UserAlreadyActiveException.php index b3ad6794b..3490eae6d 100644 --- a/src/Exception/UserAlreadyActiveException.php +++ b/src/Exception/UserAlreadyActiveException.php @@ -13,12 +13,11 @@ namespace CakeDC\Users\Exception; -use Cake\Core\Exception\Exception; - -class UserAlreadyActiveException extends Exception +class UserAlreadyActiveException extends \Cake\Core\Exception\CakeException { /** * UserAlreadyActiveException constructor. + * * @param array|string $message message * @param int $code code * @param null $previous previous diff --git a/src/Exception/UserNotActiveException.php b/src/Exception/UserNotActiveException.php index c9a4c1d26..5533b4447 100644 --- a/src/Exception/UserNotActiveException.php +++ b/src/Exception/UserNotActiveException.php @@ -13,12 +13,11 @@ namespace CakeDC\Users\Exception; -use Cake\Core\Exception\Exception; - -class UserNotActiveException extends Exception +class UserNotActiveException extends \Cake\Core\Exception\CakeException { /** * UserNotActiveException constructor. + * * @param array|string $message message * @param int $code code * @param null $previous previous diff --git a/src/Exception/UserNotFoundException.php b/src/Exception/UserNotFoundException.php index bafbbb679..0847369d8 100644 --- a/src/Exception/UserNotFoundException.php +++ b/src/Exception/UserNotFoundException.php @@ -19,6 +19,7 @@ class UserNotFoundException extends RecordNotFoundException { /** * UserNotFoundException constructor. + * * @param string $message message * @param int $code code * @param null $previous previous diff --git a/src/Exception/WrongPasswordException.php b/src/Exception/WrongPasswordException.php index 0d0494ca7..8a4ec0b56 100644 --- a/src/Exception/WrongPasswordException.php +++ b/src/Exception/WrongPasswordException.php @@ -13,12 +13,11 @@ namespace CakeDC\Users\Exception; -use Cake\Core\Exception\Exception; - -class WrongPasswordException extends Exception +class WrongPasswordException extends \Cake\Core\Exception\CakeException { /** * WrongPasswordException constructor. + * * @param array|string $message message * @param int $code code * @param null $previous previous diff --git a/src/Identifier/SocialIdentifier.php b/src/Identifier/SocialIdentifier.php index 2448c3c0f..7a75ecdc4 100644 --- a/src/Identifier/SocialIdentifier.php +++ b/src/Identifier/SocialIdentifier.php @@ -56,19 +56,16 @@ public function identify(array $credentials) } if ($user->get('social_accounts')) { - $this->dispatchEvent(Plugin::EVENT_AFTER_REGISTER, compact('user')); + $this->dispatchEvent(Plugin::EVENT_AFTER_REGISTER, ['user' => $user]); } - $user = $this->findUser($user)->firstOrFail(); - - return $user; + return $this->findUser($user)->firstOrFail(); } /** * Get query object for fetching user from database. * * @param \Cake\Datasource\EntityInterface $user The user. - * * @return \Cake\ORM\Query */ protected function findUser($user) @@ -90,7 +87,6 @@ protected function findUser($user) * Create a new user or get if exists one for the social data * * @param mixed $data social data - * * @return mixed */ protected function createOrGetUser($data) diff --git a/src/Loader/AuthenticationServiceLoader.php b/src/Loader/AuthenticationServiceLoader.php index 1b99b3726..c4d2b6958 100644 --- a/src/Loader/AuthenticationServiceLoader.php +++ b/src/Loader/AuthenticationServiceLoader.php @@ -61,7 +61,6 @@ protected function loadIdentifiers($service) * Load the authenticators defined at config Auth.Authenticators * * @param \CakeDC\Auth\Authentication\AuthenticationService $service Authentication service to load identifiers - * * @return void */ protected function loadAuthenticators($service) @@ -79,7 +78,6 @@ protected function loadAuthenticators($service) * Load the CakeDC/Auth.TwoFactor based on config OneTimePasswordAuthenticator.login * * @param \CakeDC\Auth\Authentication\AuthenticationService $service Authentication service to load identifiers - * * @return void */ protected function loadTwoFactorAuthenticator($service) @@ -108,7 +106,7 @@ protected function _getItemLoadData($item, $key) $options = $item; if (!isset($options['className'])) { throw new \InvalidArgumentException( - __('Property {0}.className should be defined', $key) + __d('cake_d_c/users', 'Property {0}.className should be defined', $key) ); } $className = $options['className']; diff --git a/src/Loader/LoginComponentLoader.php b/src/Loader/LoginComponentLoader.php index f309d3be2..29e6a235c 100644 --- a/src/Loader/LoginComponentLoader.php +++ b/src/Loader/LoginComponentLoader.php @@ -32,7 +32,7 @@ public static function forForm($controller) 'messages' => [ 'FAILURE_INVALID_RECAPTCHA' => __d('cake_d_c/users', 'Invalid reCaptcha'), ], - 'targetAuthenticator' => 'CakeDC\Auth\Authenticator\FormAuthenticator', + 'targetAuthenticator' => \CakeDC\Auth\Authenticator\FormAuthenticator::class, ]; return self::createComponent($controller, 'Auth.FormLoginFailure', $config); @@ -60,7 +60,7 @@ public static function forSocial($controller) 'Your social account has not been validated yet. Please check your inbox for instructions' ), ], - 'targetAuthenticator' => 'CakeDC\Users\Authenticator\SocialAuthenticator', + 'targetAuthenticator' => \CakeDC\Users\Authenticator\SocialAuthenticator::class, ]; return self::createComponent($controller, 'Auth.SocialLoginFailure', $config); @@ -72,7 +72,6 @@ public static function forSocial($controller) * @param \Cake\Controller\Controller $controller Target controller * @param string $key configuration key * @param array $config base configuration - * * @return \CakeDC\Users\Controller\Component\LoginComponent|\Cake\Controller\Component * @throws \Exception */ diff --git a/src/Loader/MiddlewareQueueLoader.php b/src/Loader/MiddlewareQueueLoader.php index a1f4cca97..cb140d87c 100644 --- a/src/Loader/MiddlewareQueueLoader.php +++ b/src/Loader/MiddlewareQueueLoader.php @@ -13,7 +13,9 @@ namespace CakeDC\Users\Loader; +use Authentication\AuthenticationServiceProviderInterface; use Authentication\Middleware\AuthenticationMiddleware; +use Authorization\AuthorizationServiceProviderInterface; use Authorization\Middleware\AuthorizationMiddleware; use Authorization\Middleware\RequestAuthorizationMiddleware; use Cake\Core\Configure; @@ -21,7 +23,6 @@ use CakeDC\Auth\Middleware\TwoFactorMiddleware; use CakeDC\Users\Middleware\SocialAuthMiddleware; use CakeDC\Users\Middleware\SocialEmailMiddleware; -use CakeDC\Users\Plugin; /** * Class MiddlewareQueueLoader @@ -40,24 +41,26 @@ class MiddlewareQueueLoader * For 'Auth.Authorization.loadRbacMiddleware' load RbacMiddleware * * @param \Cake\Http\MiddlewareQueue $middlewareQueue The middleware queue to update. - * @param \CakeDC\Users\Plugin $plugin Users plugin object - * + * @param \Authentication\AuthenticationServiceProviderInterface $authenticationServiceProvider Loads the auth service + * @param \Authorization\AuthorizationServiceProviderInterface $authorizationServiceProvider Loads the authorization service * @return \Cake\Http\MiddlewareQueue */ - public function __invoke(MiddlewareQueue $middlewareQueue, Plugin $plugin) - { + public function __invoke( + MiddlewareQueue $middlewareQueue, + AuthenticationServiceProviderInterface $authenticationServiceProvider, + AuthorizationServiceProviderInterface $authorizationServiceProvider + ) { $this->loadSocialMiddleware($middlewareQueue); - $this->loadAuthenticationMiddleware($middlewareQueue, $plugin); + $this->loadAuthenticationMiddleware($middlewareQueue, $authenticationServiceProvider); $this->load2faMiddleware($middlewareQueue); - return $this->loadAuthorizationMiddleware($middlewareQueue, $plugin); + return $this->loadAuthorizationMiddleware($middlewareQueue, $authorizationServiceProvider); } /** * Load social middlewares if enabled. Based on config 'Users.Social.login' * * @param \Cake\Http\MiddlewareQueue $middlewareQueue The middleware queue to update. - * * @return void */ protected function loadSocialMiddleware(MiddlewareQueue $middlewareQueue) @@ -73,13 +76,14 @@ protected function loadSocialMiddleware(MiddlewareQueue $middlewareQueue) * Load authentication middleware * * @param \Cake\Http\MiddlewareQueue $middlewareQueue queue of middleware - * @param \CakeDC\Users\Plugin $plugin Users plugin object - * + * @param \Authentication\AuthenticationServiceProviderInterface $authenticationServiceProvider Authentication service provider * @return void */ - protected function loadAuthenticationMiddleware(MiddlewareQueue $middlewareQueue, Plugin $plugin) - { - $authentication = new AuthenticationMiddleware($plugin); + protected function loadAuthenticationMiddleware( + MiddlewareQueue $middlewareQueue, + AuthenticationServiceProviderInterface $authenticationServiceProvider + ) { + $authentication = new AuthenticationMiddleware($authenticationServiceProvider); $middlewareQueue->add($authentication); } @@ -87,7 +91,6 @@ protected function loadAuthenticationMiddleware(MiddlewareQueue $middlewareQueue * Load OneTimePasswordAuthenticatorMiddleware if enabled. Based on config 'OneTimePasswordAuthenticator.login' * * @param \Cake\Http\MiddlewareQueue $middlewareQueue queue of middleware - * * @return void */ protected function load2faMiddleware(MiddlewareQueue $middlewareQueue) @@ -104,16 +107,22 @@ protected function load2faMiddleware(MiddlewareQueue $middlewareQueue) * Load authorization middleware based on Auth.Authorization. * * @param \Cake\Http\MiddlewareQueue $middlewareQueue queue of middleware - * @param \CakeDC\Users\Plugin $plugin Users plugin object - * + * @param \Authorization\AuthorizationServiceProviderInterface $authorizationServiceProvider Authorization service provider * @return \Cake\Http\MiddlewareQueue */ - protected function loadAuthorizationMiddleware(MiddlewareQueue $middlewareQueue, Plugin $plugin) - { + protected function loadAuthorizationMiddleware( + MiddlewareQueue $middlewareQueue, + AuthorizationServiceProviderInterface $authorizationServiceProvider + ) { if (Configure::read('Auth.Authorization.enable') === false) { return $middlewareQueue; } - $middlewareQueue->add(new AuthorizationMiddleware($plugin, Configure::read('Auth.AuthorizationMiddleware'))); + $middlewareQueue->add( + new AuthorizationMiddleware( + $authorizationServiceProvider, + Configure::read('Auth.AuthorizationMiddleware') + ) + ); if (Configure::read('Auth.AuthorizationMiddleware.requireAuthorizationCheck') !== false) { $middlewareQueue->add(new RequestAuthorizationMiddleware()); } diff --git a/src/Mailer/UsersMailer.php b/src/Mailer/UsersMailer.php index 7e3ab2985..2e4070e96 100644 --- a/src/Mailer/UsersMailer.php +++ b/src/Mailer/UsersMailer.php @@ -19,7 +19,6 @@ /** * User Mailer - * */ class UsersMailer extends Mailer { @@ -56,7 +55,6 @@ protected function validation(EntityInterface $user) * Send the reset password email to the user * * @param \Cake\Datasource\EntityInterface $user User entity - * * @return void */ protected function resetPassword(EntityInterface $user) @@ -88,7 +86,6 @@ protected function resetPassword(EntityInterface $user) * * @param \Cake\Datasource\EntityInterface $user User entity * @param \Cake\Datasource\EntityInterface $socialAccount SocialAccount entity - * * @return void */ protected function socialAccountValidation(EntityInterface $user, EntityInterface $socialAccount) @@ -110,7 +107,7 @@ protected function socialAccountValidation(EntityInterface $user, EntityInterfac ->setTo($user['email']) ->setSubject($subject) ->setEmailFormat(Message::MESSAGE_BOTH) - ->setViewVars(compact('user', 'socialAccount', 'activationUrl')); + ->setViewVars(['user' => $user, 'socialAccount' => $socialAccount, 'activationUrl' => $activationUrl]); $this ->viewBuilder() ->setTemplate('CakeDC/Users.socialAccountValidation'); diff --git a/src/Middleware/SocialAuthMiddleware.php b/src/Middleware/SocialAuthMiddleware.php index 8548d433a..00ca65130 100644 --- a/src/Middleware/SocialAuthMiddleware.php +++ b/src/Middleware/SocialAuthMiddleware.php @@ -37,12 +37,11 @@ class SocialAuthMiddleware implements MiddlewareInterface * * @param \Cake\Http\ServerRequest $request The request. * @param \CakeDC\Users\Exception\SocialAuthenticationException $exception Exception thrown - * * @return \Psr\Http\Message\ResponseInterface A response */ protected function onAuthenticationException(ServerRequest $request, $exception) { - $baseClassName = get_class($exception->getPrevious()); + $baseClassName = $exception->getPrevious() !== null ? get_class($exception->getPrevious()) : self::class; $response = new Response(); if ($baseClassName === MissingEmailException::class) { $this->setErrorMessage($request, __d('cake_d_c/users', 'Please enter your email')); @@ -65,7 +64,6 @@ protected function onAuthenticationException(ServerRequest $request, $exception) * * @param \Cake\Http\ServerRequest $request the request with session attribute * @param string $message the message - * * @return void */ private function setErrorMessage(ServerRequest $request, $message) @@ -99,7 +97,6 @@ protected function responseWithActionLocation(Response $response, $action) * * @param \Cake\Http\ServerRequest $request The request * @param \Psr\Http\Server\RequestHandlerInterface $handler The request handler. - * * @return \Psr\Http\Message\ResponseInterface */ protected function goNext(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface diff --git a/src/Middleware/UnauthorizedHandler/DefaultRedirectHandler.php b/src/Middleware/UnauthorizedHandler/DefaultRedirectHandler.php index 2c3d3d948..709a3fe43 100644 --- a/src/Middleware/UnauthorizedHandler/DefaultRedirectHandler.php +++ b/src/Middleware/UnauthorizedHandler/DefaultRedirectHandler.php @@ -89,7 +89,6 @@ protected function getUrl(ServerRequestInterface $request, array $options): stri * * @param \Cake\Http\Session $session The CakePHP session. * @param array $options Defined options. - * * @return void */ protected function addFlashMessage(Session $session, $options): void @@ -108,13 +107,12 @@ protected function addFlashMessage(Session $session, $options): void protected function createFlashMessage($options): array { $message = (array)($options['flash'] ?? []); - $message += [ + + return $message + [ 'message' => __d('cake_d_c/users', 'You are not authorized to access that location.'), 'key' => 'flash', 'element' => 'flash/error', 'params' => [], ]; - - return $message; } } diff --git a/src/Model/Behavior/AuthFinderBehavior.php b/src/Model/Behavior/AuthFinderBehavior.php index 6add37b39..3dc19fe2b 100644 --- a/src/Model/Behavior/AuthFinderBehavior.php +++ b/src/Model/Behavior/AuthFinderBehavior.php @@ -51,7 +51,7 @@ public function findAuth(Query $query, array $options = []) $where = $query->clause('where') ?: []; $query ->where(function ($exp) use ($identifier, $where) { - $or = $exp->or_([$this->_table->aliasField('email') => $identifier]); + $or = $exp->or([$this->_table->aliasField('email') => $identifier]); return $or->add($where); }, [], true) diff --git a/src/Model/Behavior/BaseTokenBehavior.php b/src/Model/Behavior/BaseTokenBehavior.php index 0d4dfbe6e..fbbe3cc89 100644 --- a/src/Model/Behavior/BaseTokenBehavior.php +++ b/src/Model/Behavior/BaseTokenBehavior.php @@ -14,7 +14,7 @@ namespace CakeDC\Users\Model\Behavior; use Cake\Datasource\EntityInterface; -use Cake\I18n\Time; +use Cake\I18n\FrozenTime; use Cake\ORM\Behavior; /** @@ -38,7 +38,7 @@ protected function _updateActive(EntityInterface $user, $validateEmail, $tokenEx $user->updateToken($tokenExpiration); } else { $user['active'] = true; - $user['activation_date'] = new Time(); + $user['activation_date'] = new FrozenTime(); } return $user; @@ -54,8 +54,7 @@ protected function _removeValidationToken(EntityInterface $user) { $user['token'] = null; $user['token_expires'] = null; - $result = $this->_table->save($user); - return $result; + return $this->_table->save($user); } } diff --git a/src/Model/Behavior/LinkSocialBehavior.php b/src/Model/Behavior/LinkSocialBehavior.php index b88f03b72..14096acea 100644 --- a/src/Model/Behavior/LinkSocialBehavior.php +++ b/src/Model/Behavior/LinkSocialBehavior.php @@ -14,7 +14,7 @@ namespace CakeDC\Users\Model\Behavior; use Cake\Datasource\EntityInterface; -use Cake\I18n\Time; +use Cake\I18n\FrozenTime; use Cake\ORM\Behavior; /** @@ -34,7 +34,6 @@ class LinkSocialBehavior extends Behavior * * @param \Cake\Datasource\EntityInterface $user User to link. * @param array $data Social account information. - * * @return \Cake\Datasource\EntityInterface */ public function linkSocialAccount(EntityInterface $user, $data) @@ -66,7 +65,6 @@ public function linkSocialAccount(EntityInterface $user, $data) * @param \Cake\Datasource\EntityInterface $user User to link. * @param array $data Social account information. * @param \Cake\Datasource\EntityInterface $socialAccount to update or create. - * * @return \Cake\Datasource\EntityInterface */ protected function createOrUpdateSocialAccount(EntityInterface $user, $data, $socialAccount) @@ -107,7 +105,6 @@ protected function createOrUpdateSocialAccount(EntityInterface $user, $data, $so * * @param \Cake\Datasource\EntityInterface $socialAccount to populate. * @param array $data Social account information. - * * @return \Cake\Datasource\EntityInterface */ protected function populateSocialAccount($socialAccount, $data) @@ -117,7 +114,9 @@ protected function populateSocialAccount($socialAccount, $data) $accountData['reference'] = $data['id'] ?? null; $accountData['avatar'] = $data['avatar'] ?? null; $accountData['link'] = $data['link'] ?? null; - $accountData['avatar'] = str_replace('normal', 'square', $accountData['avatar']); + if ($accountData['avatar'] ?? null) { + $accountData['avatar'] = str_replace('normal', 'square', $accountData['avatar']); + } $accountData['description'] = $data['bio'] ?? null; $accountData['token'] = $data['credentials']['token'] ?? null; $accountData['token_secret'] = $data['credentials']['secret'] ?? null; @@ -125,7 +124,7 @@ protected function populateSocialAccount($socialAccount, $data) $accountData['token_expires'] = null; $expires = $data['credentials']['expires'] ?? null; if (!empty($expires)) { - $expiresTime = new Time(); + $expiresTime = new FrozenTime(); $accountData['token_expires'] = $expiresTime->setTimestamp($expires)->format('Y-m-d H:i:s'); } diff --git a/src/Model/Behavior/PasswordBehavior.php b/src/Model/Behavior/PasswordBehavior.php index a434b6008..e307d15a0 100644 --- a/src/Model/Behavior/PasswordBehavior.php +++ b/src/Model/Behavior/PasswordBehavior.php @@ -34,7 +34,6 @@ class PasswordBehavior extends BaseTokenBehavior * * @param string $reference User username or email * @param array $options checkActive, sendEmail, expiration - * * @return string * @throws \InvalidArgumentException * @throws \CakeDC\Users\Exception\UserNotFoundException @@ -43,30 +42,28 @@ class PasswordBehavior extends BaseTokenBehavior public function resetToken($reference, array $options = []) { if (empty($reference)) { - throw new \InvalidArgumentException(__d('cake_d_c/users', "Reference cannot be null")); + throw new \InvalidArgumentException(__d('cake_d_c/users', 'Reference cannot be null')); } $expiration = $options['expiration'] ?? null; if (empty($expiration)) { - throw new \InvalidArgumentException(__d('cake_d_c/users', "Token expiration cannot be empty")); + throw new \InvalidArgumentException(__d('cake_d_c/users', 'Token expiration cannot be empty')); } $user = $this->_getUser($reference); if (empty($user)) { - throw new UserNotFoundException(__d('cake_d_c/users', "User not found")); + throw new UserNotFoundException(__d('cake_d_c/users', 'User not found')); } if ($options['checkActive'] ?? false) { if ($user->active) { - throw new UserAlreadyActiveException(__d('cake_d_c/users', "User account already validated")); + throw new UserAlreadyActiveException(__d('cake_d_c/users', 'User account already validated')); } $user->active = false; $user->activation_date = null; } - if ($options['ensureActive'] ?? false) { - if (!$user['active']) { - throw new UserNotActiveException(__d('cake_d_c/users', "User not active")); - } + if (($options['ensureActive'] ?? false) && !$user['active']) { + throw new UserNotActiveException(__d('cake_d_c/users', 'User not active')); } $user->updateToken($expiration); $saveResult = $this->_table->save($user); @@ -136,7 +133,7 @@ public function changePassword(EntityInterface $user) 'contain' => [], ]); } catch (RecordNotFoundException $e) { - throw new UserNotFoundException(__d('cake_d_c/users', "User not found")); + throw new UserNotFoundException(__d('cake_d_c/users', 'User not found')); } if (!empty($user->current_password)) { diff --git a/src/Model/Behavior/RegisterBehavior.php b/src/Model/Behavior/RegisterBehavior.php index 891b13566..b2fb4fb10 100644 --- a/src/Model/Behavior/RegisterBehavior.php +++ b/src/Model/Behavior/RegisterBehavior.php @@ -15,7 +15,6 @@ use Cake\Core\Configure; use Cake\Datasource\EntityInterface; -use Cake\Event\Event; use Cake\Mailer\MailerAwareTrait; use Cake\Validation\Validator; use CakeDC\Users\Exception\TokenExpiredException; @@ -29,6 +28,15 @@ class RegisterBehavior extends BaseTokenBehavior { use MailerAwareTrait; + /** + * @var bool + */ + protected $validateEmail; + /** + * @var bool + */ + protected $useTos; + /** * Constructor hook method. * @@ -55,10 +63,17 @@ public function register($user, $data, $options) $validateEmail = $options['validate_email'] ?? null; $tokenExpiration = $options['token_expiration'] ?? null; $validator = $options['validator'] ?? null; + if (is_string($validator)) { + $validate = $validator; + } else { + $this->_table->setValidator('current', $validator ?: $this->getRegisterValidators($options)); + $validate = 'current'; + } + $user = $this->_table->patchEntity( $user, $data, - ['validate' => $validator ?: $this->getRegisterValidators($options)] + ['validate' => $validate] ); $user['role'] = Configure::read('Users.Registration.defaultRole') ?: 'user'; $user->validated = false; @@ -89,10 +104,10 @@ public function validate($token, $callback = null) ->where(['token' => $token]) ->first() : null; if (empty($user)) { - throw new UserNotFoundException(__d('cake_d_c/users', "User not found for the given token and email.")); + throw new UserNotFoundException(__d('cake_d_c/users', 'User not found for the given token and email.')); } if ($user->tokenExpired()) { - throw new TokenExpiredException(__d('cake_d_c/users', "Token has already expired user with no token")); + throw new TokenExpiredException(__d('cake_d_c/users', 'Token has already expired user with no token')); } if (!method_exists($this, (string)$callback)) { return $user; @@ -111,14 +126,13 @@ public function validate($token, $callback = null) public function activateUser(EntityInterface $user) { if ($user->active) { - throw new UserAlreadyActiveException(__d('cake_d_c/users', "User account already validated")); + throw new UserAlreadyActiveException(__d('cake_d_c/users', 'User account already validated')); } $user->activation_date = new \DateTime(); $user->token_expires = null; $user->active = true; - $result = $this->_table->save($user); - return $result; + return $this->_table->save($user); } /** @@ -129,7 +143,7 @@ public function activateUser(EntityInterface $user) * @param string $name name * @return \Cake\Validation\Validator */ - public function buildValidator(Event $event, Validator $validator, $name) + public function buildValidator(\Cake\Event\EventInterface $event, Validator $validator, $name) { if ($name === 'default') { return $this->_emailValidator($validator, $this->validateEmail); diff --git a/src/Model/Behavior/SocialAccountBehavior.php b/src/Model/Behavior/SocialAccountBehavior.php index 3fff00472..10408a060 100644 --- a/src/Model/Behavior/SocialAccountBehavior.php +++ b/src/Model/Behavior/SocialAccountBehavior.php @@ -38,11 +38,7 @@ class SocialAccountBehavior extends Behavior public function initialize(array $config): void { parent::initialize($config); - $this->_table->belongsTo('Users', [ - 'foreignKey' => 'user_id', - 'joinType' => 'INNER', - 'className' => Configure::read('Users.table'), - ]); + $this->_table->belongsTo('Users')->setForeignKey('user_id')->setJoinType('INNER')->setClassName(Configure::read('Users.table')); } /** @@ -101,11 +97,11 @@ public function validateAccount($provider, $reference, $token) if (!empty($socialAccount) && $socialAccount->token === $token) { if ($socialAccount->active) { - throw new AccountAlreadyActiveException(__d('cake_d_c/users', "Account already validated")); + throw new AccountAlreadyActiveException(__d('cake_d_c/users', 'Account already validated')); } } else { throw new RecordNotFoundException( - __d('cake_d_c/users', "Account not found for the given token and email.") + __d('cake_d_c/users', 'Account not found for the given token and email.') ); } @@ -131,12 +127,12 @@ public function resendValidation($provider, $reference) if (!empty($socialAccount)) { if ($socialAccount->active) { throw new AccountAlreadyActiveException( - __d('cake_d_c/users', "Account already validated") + __d('cake_d_c/users', 'Account already validated') ); } } else { throw new RecordNotFoundException( - __d('cake_d_c/users', "Account not found for the given token and email.") + __d('cake_d_c/users', 'Account not found for the given token and email.') ); } @@ -152,8 +148,7 @@ public function resendValidation($provider, $reference) protected function _activateAccount($socialAccount) { $socialAccount->active = true; - $result = $this->_table->save($socialAccount); - return $result; + return $this->_table->save($socialAccount); } } diff --git a/src/Model/Behavior/SocialBehavior.php b/src/Model/Behavior/SocialBehavior.php index 37ad1e291..7911ed802 100644 --- a/src/Model/Behavior/SocialBehavior.php +++ b/src/Model/Behavior/SocialBehavior.php @@ -27,7 +27,6 @@ /** * Covers social features - * */ class SocialBehavior extends BaseTokenBehavior { @@ -136,7 +135,7 @@ protected function _createSocialUser($data, $options = []) if ($useEmail && empty($email)) { throw new MissingEmailException(__d('cake_d_c/users', 'Email not present')); } else { - $existingUser = $this->_table->find('existingForSocialLogin', compact('email'))->first(); + $existingUser = $this->_table->find('existingForSocialLogin', ['email' => $email])->first(); } $user = $this->_populateUser($data, $existingUser, $useEmail, $validateEmail, $tokenExpiration); @@ -151,9 +150,8 @@ protected function _createSocialUser($data, $options = []) } $this->_table->isValidateEmail = $validateEmail; - $result = $this->_table->save($user); - return $result; + return $this->_table->save($user); } /** @@ -170,6 +168,7 @@ protected function _createSocialUser($data, $options = []) */ protected function _populateUser($data, $existingUser, $useEmail, $validateEmail, $tokenExpiration) { + $userData = []; $accountData = $this->extractAccountData($data); $accountData['active'] = true; @@ -213,7 +212,7 @@ protected function _populateUser($data, $existingUser, $useEmail, $validateEmail $userData['password'] = $this->randomString(); $userData['avatar'] = $data['avatar'] ?? null; $userData['validated'] = !empty($dataValidated); - $userData['tos_date'] = date("Y-m-d H:i:s"); + $userData['tos_date'] = date('Y-m-d H:i:s'); $userData['gender'] = $data['gender'] ?? null; $userData['social_accounts'][] = $accountData; @@ -248,7 +247,7 @@ public function generateUniqueUsername($username) ->where([$this->_table->aliasField($this->_username) => $username]) ->count(); if ($existingUsername > 0) { - $username = $username . $i; + $username .= $i; $i++; continue; } @@ -263,7 +262,6 @@ public function generateUniqueUsername($username) * * @param \Cake\ORM\Query $query The base query. * @param array $options Find options with email key. - * * @return \Cake\ORM\Query */ public function findExistingForSocialLogin(\Cake\ORM\Query $query, array $options) @@ -277,7 +275,6 @@ public function findExistingForSocialLogin(\Cake\ORM\Query $query, array $option * Extract the account data to insert/update * * @param array $data Social data. - * * @throws \Exception * @return array */ @@ -289,7 +286,9 @@ protected function extractAccountData(array $data) $accountData['avatar'] = $data['avatar'] ?? null; $accountData['link'] = $data['link'] ?? null; - $accountData['avatar'] = str_replace('normal', 'square', $accountData['avatar']); + if ($accountData['avatar'] ?? null) { + $accountData['avatar'] = str_replace('normal', 'square', $accountData['avatar']); + } $accountData['description'] = $data['bio'] ?? null; $accountData['token'] = $data['credentials']['token'] ?? null; $accountData['token_secret'] = $data['credentials']['secret'] ?? null; diff --git a/src/Model/Entity/User.php b/src/Model/Entity/User.php index 45e162089..e007fc912 100644 --- a/src/Model/Entity/User.php +++ b/src/Model/Entity/User.php @@ -14,7 +14,7 @@ namespace CakeDC\Users\Model\Entity; use Cake\Core\Configure; -use Cake\I18n\Time; +use Cake\I18n\FrozenTime; use Cake\ORM\Entity; use Cake\Utility\Security; @@ -25,9 +25,10 @@ * @property string $role * @property string $username * @property bool $is_superuser - * @property \Cake\I18n\Time $token_expires + * @property \Cake\I18n\Time|\Cake\I18n\FrozenTime $token_expires * @property string $token - * @property array $additional_data + * @property string $api_token + * @property array|string $additional_data * @property \CakeDC\Users\Model\Entity\SocialAccount[] $social_accounts */ class User extends Entity @@ -80,8 +81,8 @@ protected function _setConfirmPassword($password) */ protected function _setTos($tos) { - if ((bool)$tos === true) { - $this->set('tos_date', Time::now()); + if ((bool)$tos) { + $this->set('tos_date', FrozenTime::now()); } return $tos; @@ -110,7 +111,7 @@ public function getPasswordHasher() { $passwordHasher = Configure::read('Users.passwordHasher'); if (!class_exists($passwordHasher)) { - $passwordHasher = '\Cake\Auth\DefaultPasswordHasher'; + $passwordHasher = \Cake\Auth\DefaultPasswordHasher::class; } return new $passwordHasher(); @@ -141,7 +142,7 @@ public function tokenExpired() return true; } - return new Time($this->token_expires) < Time::now(); + return new FrozenTime($this->token_expires) < FrozenTime::now(); } /** @@ -166,22 +167,26 @@ protected function _getAvatar() */ protected function _getU2fRegistration() { + if (is_string($this->additional_data)) { + $this->additional_data = json_decode($this->additional_data, true); + } if (!isset($this->additional_data['u2f_registration'])) { return null; } $object = (object)$this->additional_data['u2f_registration']; - return isset($object->keyHandle) ? $object : null; + return $object->keyHandle !== null ? $object : null; } /** * Generate token_expires and token in a user + * * @param int $tokenExpiration seconds to expire the token from Now * @return void */ public function updateToken($tokenExpiration = 0) { - $expiration = new Time('now'); + $expiration = new FrozenTime('now'); $this->token_expires = $expiration->addSeconds($tokenExpiration); $this->token = bin2hex(Security::randomBytes(16)); } diff --git a/src/Model/Table/UsersTable.php b/src/Model/Table/UsersTable.php index 83c8ad049..4e6954348 100644 --- a/src/Model/Table/UsersTable.php +++ b/src/Model/Table/UsersTable.php @@ -29,7 +29,6 @@ * @method \CakeDC\Users\Model\Entity\User patchEntity(\Cake\Datasource\EntityInterface $entity, array $data, array $options = []) * @method \CakeDC\Users\Model\Entity\User[] patchEntities($entities, array $data, array $options = []) * @method \CakeDC\Users\Model\Entity\User findOrCreate($search, callable $callback = null, $options = []) - * * @mixin \CakeDC\Users\Model\Behavior\AuthFinderBehavior * @mixin \CakeDC\Users\Model\Behavior\LinkSocialBehavior * @mixin \CakeDC\Users\Model\Behavior\PasswordBehavior @@ -84,14 +83,12 @@ public function initialize(array $config): void $this->addBehavior('CakeDC/Users.Social'); $this->addBehavior('CakeDC/Users.LinkSocial'); $this->addBehavior('CakeDC/Users.AuthFinder'); - $this->hasMany('SocialAccounts', [ - 'foreignKey' => 'user_id', - 'className' => 'CakeDC/Users.SocialAccounts', - ]); + $this->hasMany('SocialAccounts')->setForeignKey('user_id')->setClassName('CakeDC/Users.SocialAccounts'); } /** * Adds some rules for password confirm + * * @param \Cake\Validation\Validator $validator Cake validator object. * @return \Cake\Validation\Validator */ @@ -179,16 +176,15 @@ public function validationDefault(Validator $validator): Validator /** * Wrapper for all validation rules for register - * @param \Cake\Validation\Validator $validator Cake validator object. * + * @param \Cake\Validation\Validator $validator Cake validator object. * @return \Cake\Validation\Validator */ public function validationRegister(Validator $validator) { $validator = $this->validationDefault($validator); - $validator = $this->validationPasswordConfirm($validator); - return $validator; + return $this->validationPasswordConfirm($validator); } /** diff --git a/src/Plugin.php b/src/Plugin.php index 6b5fc5d91..b39c99565 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -13,17 +13,16 @@ namespace CakeDC\Users; -use Authentication\AuthenticationServiceInterface; -use Authentication\AuthenticationServiceProviderInterface; -use Authorization\AuthorizationServiceInterface; -use Authorization\AuthorizationServiceProviderInterface; use Cake\Core\BasePlugin; -use Cake\Core\Configure; use Cake\Http\MiddlewareQueue; -use Psr\Http\Message\ServerRequestInterface; +use CakeDC\Users\Provider\AuthenticationServiceProvider; +use CakeDC\Users\Provider\AuthorizationServiceProvider; +use CakeDC\Users\Provider\ServiceProviderLoaderTrait; -class Plugin extends BasePlugin implements AuthenticationServiceProviderInterface, AuthorizationServiceProviderInterface +class Plugin extends BasePlugin { + use ServiceProviderLoaderTrait; + /** * Plugin name. * @@ -42,68 +41,19 @@ class Plugin extends BasePlugin implements AuthenticationServiceProviderInterfac public const EVENT_SOCIAL_LOGIN_EXISTING_ACCOUNT = 'Users.Global.socialLoginExistingAccount'; public const EVENT_ON_EXPIRED_TOKEN = 'Users.Global.onExpiredToken'; public const EVENT_AFTER_RESEND_TOKEN_VALIDATION = 'Users.Global.afterResendTokenValidation'; + public const EVENT_AFTER_EMAIL_TOKEN_VALIDATION = 'Users.Global.afterEmailTokenValidation'; /** - * Returns an authentication service instance. - * - * @param \Psr\Http\Message\ServerRequestInterface $request Request - * @return \Authentication\AuthenticationServiceInterface - */ - public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface - { - $key = 'Auth.Authentication.serviceLoader'; - - return $this->loadService($request, $key); - } - - /** - * {@inheritdoc} - */ - public function getAuthorizationService(ServerRequestInterface $request): AuthorizationServiceInterface - { - $key = 'Auth.Authorization.serviceLoader'; - - return $this->loadService($request, $key); - } - - /** - * {@inheritdoc} + * @inheritDoc */ public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue { $loader = $this->getLoader('Users.middlewareQueueLoader'); - return $loader($middlewareQueue, $this); - } - - /** - * Load a service defined in configuration $loaderKey - * - * @param \Psr\Http\Message\ServerRequestInterface $request The request. - * @param string $loaderKey service loader key - * - * @return mixed - */ - protected function loadService(ServerRequestInterface $request, $loaderKey) - { - $serviceLoader = $this->getLoader($loaderKey); - - return $serviceLoader($request); - } - - /** - * Get the loader callable - * - * @param string $loaderKey loader configuration key - * @return callable - */ - protected function getLoader($loaderKey) - { - $serviceLoader = Configure::read($loaderKey); - if (is_string($serviceLoader)) { - $serviceLoader = new $serviceLoader(); - } - - return $serviceLoader; + return $loader( + $middlewareQueue, + new AuthenticationServiceProvider(), + new AuthorizationServiceProvider() + ); } } diff --git a/src/Provider/AuthenticationServiceProvider.php b/src/Provider/AuthenticationServiceProvider.php new file mode 100644 index 000000000..1c4dcc3e4 --- /dev/null +++ b/src/Provider/AuthenticationServiceProvider.php @@ -0,0 +1,39 @@ +loadService($request, $key); + } +} diff --git a/src/Provider/AuthorizationServiceProvider.php b/src/Provider/AuthorizationServiceProvider.php new file mode 100644 index 000000000..8c727f41a --- /dev/null +++ b/src/Provider/AuthorizationServiceProvider.php @@ -0,0 +1,39 @@ +loadService($request, $key); + } +} diff --git a/src/Provider/ServiceProviderLoaderTrait.php b/src/Provider/ServiceProviderLoaderTrait.php new file mode 100644 index 000000000..ccfc0183c --- /dev/null +++ b/src/Provider/ServiceProviderLoaderTrait.php @@ -0,0 +1,55 @@ +getLoader($loaderKey); + + return $serviceLoader($request); + } + + /** + * Get the loader callable + * + * @param string $loaderKey loader configuration key + * @return callable + */ + protected function getLoader($loaderKey) + { + $serviceLoader = Configure::read($loaderKey); + if (is_string($serviceLoader)) { + $serviceLoader = new $serviceLoader(); + } + + return $serviceLoader; + } +} diff --git a/src/Shell/UsersShell.php b/src/Shell/UsersShell.php index 6446d09fa..bd38e2e44 100644 --- a/src/Shell/UsersShell.php +++ b/src/Shell/UsersShell.php @@ -38,7 +38,6 @@ class UsersShell extends Shell ]; /** - * * @return \Cake\Console\ConsoleOptionParser */ public function getOptionParser(): ConsoleOptionParser @@ -57,6 +56,9 @@ public function getOptionParser(): ConsoleOptionParser ->addSubcommand('changeRole', [ 'help' => __d('cake_d_c/users', 'Change the role for an specific user'), ]) + ->addSubcommand('changeApiToken', [ + 'help' => __d('cake_d_c/users', 'Change the api token for an specific user'), + ]) ->addSubcommand('deactivateUser', [ 'help' => __d('cake_d_c/users', 'Deactivate an specific user'), ]) @@ -194,6 +196,40 @@ public function changeRole() $this->out(__d('cake_d_c/users', 'New role: {0}', $savedUser->role)); } + /** + * Change api token for a user + * + * Arguments: + * + * - Username + * - Token to be set + * + * @return void + */ + public function changeApiToken() + { + $username = Hash::get($this->args, 0); + $token = Hash::get($this->args, 1); + if (empty($username)) { + $this->abort(__d('cake_d_c/users', 'Please enter a username.')); + } + if (empty($token)) { + $this->abort(__d('cake_d_c/users', 'Please enter a token.')); + } + $data = [ + 'api_token' => $token, + ]; + $savedUser = $this->_updateUser($username, $data); + if (!$savedUser) { + $this->err(__d('cake_d_c/users', 'User was not saved, check validation errors')); + } + /** + * @var \CakeDC\Users\Model\Entity\User $savedUser + */ + $this->out(__d('cake_d_c/users', 'Api token changed for user: {0}', $username)); + $this->out(__d('cake_d_c/users', 'New token: {0}', $savedUser->api_token)); + } + /** * Activate an specific user * @@ -285,8 +321,8 @@ protected function _createUser($template) if (!empty($this->params['username'])) { $username = $this->params['username']; } else { - $username = !empty($template['username']) ? - $template['username'] : $this->_generateRandomUsername(); + $username = empty($template['username']) ? + $this->_generateRandomUsername() : $template['username']; } $password = (empty($this->params['password']) ? @@ -341,8 +377,6 @@ protected function _updateUser($username, $data) $user = $this->Users->find()->where(['username' => $username])->first(); if (!is_object($user)) { $this->abort(__d('cake_d_c/users', 'The user was not found.')); - - return false; } /** * @var \Cake\Datasource\EntityInterface $user @@ -353,9 +387,8 @@ protected function _updateUser($username, $data) })->each(function ($value, $field) use (&$user) { $user->{$field} = $value; }); - $savedUser = $this->Users->save($user); - return $savedUser; + return $this->Users->save($user); } /** diff --git a/src/Traits/RandomStringTrait.php b/src/Traits/RandomStringTrait.php index e1703ad2d..6caffad7b 100644 --- a/src/Traits/RandomStringTrait.php +++ b/src/Traits/RandomStringTrait.php @@ -17,6 +17,7 @@ trait RandomStringTrait { /** * Generates random string + * * @param int $length String size. * @return string */ @@ -25,7 +26,7 @@ public function randomString($length = 10) if (!is_numeric($length) || $length <= 0) { $length = 10; } - $string = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + $string = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; return substr(str_shuffle($string), 0, $length); } diff --git a/src/Utility/UsersUrl.php b/src/Utility/UsersUrl.php index 52fbd36b0..f4c2f9458 100644 --- a/src/Utility/UsersUrl.php +++ b/src/Utility/UsersUrl.php @@ -35,7 +35,6 @@ public static function isCustom() * * @param string $action user action * @param array $extra extra url attributes - * * @return array */ public static function actionUrl($action, $extra = []) @@ -75,7 +74,7 @@ public static function actionParams($action) $prefix = $parts[0]; } - return compact('prefix', 'plugin', 'controller', 'action'); + return ['prefix' => $prefix, 'plugin' => $plugin, 'controller' => $controller, 'action' => $action]; } /** @@ -83,7 +82,6 @@ public static function actionParams($action) * * @param string $action users action * @param \Cake\Http\ServerRequest $request the request - * * @return bool */ public static function checkActionOnRequest($action, ServerRequest $request) @@ -126,6 +124,7 @@ private static function getDefaultConfigUrls() 'Users.Profile.route' => static::actionUrl('profile'), 'OneTimePasswordAuthenticator.verifyAction' => static::actionUrl('verify'), 'U2f.startAction' => static::actionUrl('u2f'), + 'Webauthn2fa.startAction' => static::actionUrl('webauthn2fa'), 'Auth.AuthenticationComponent.loginAction' => $loginAction, 'Auth.AuthenticationComponent.logoutRedirect' => $loginAction, 'Auth.Authenticators.Form.loginUrl' => $loginAction, diff --git a/src/View/Helper/AuthLinkHelper.php b/src/View/Helper/AuthLinkHelper.php index ab4af59ac..06274ae37 100644 --- a/src/View/Helper/AuthLinkHelper.php +++ b/src/View/Helper/AuthLinkHelper.php @@ -18,6 +18,8 @@ /** * AuthLink helper + * + * @property \Cake\View\Helper\FormHelper $Form */ class AuthLinkHelper extends HtmlHelper { diff --git a/src/View/Helper/UserHelper.php b/src/View/Helper/UserHelper.php index c228e6ab9..00d32bd93 100644 --- a/src/View/Helper/UserHelper.php +++ b/src/View/Helper/UserHelper.php @@ -117,6 +117,7 @@ public function logout($message = null, $options = []) /** * Welcome display + * * @return string|null */ public function welcome() @@ -141,6 +142,7 @@ public function welcome() /** * Add reCaptcha script + * * @return void */ public function addReCaptchaScript() @@ -152,6 +154,7 @@ public function addReCaptchaScript() /** * Add reCaptcha to the form + * * @return mixed */ public function addReCaptcha() @@ -184,7 +187,6 @@ public function addReCaptcha() * Generate a link if the target url is authorized for the logged in user * * @deprecated Since 3.2.1. Use AuthLinkHelper::link() instead - * * @param string $title link's title. * @param string|array|null $url url that the user is making request. * @param array $options Array with option data. @@ -200,31 +202,12 @@ public function link($title, $url = null, array $options = []) return $this->AuthLink->link($title, $url, $options); } - /** - * Returns true if the target url is authorized for the logged in user - * - * @deprecated Since 3.2.1. Use AuthLinkHelper::link() instead - * - * @param string|array|null $url url that the user is making request. - * @return bool - */ - public function isAuthorized($url = null) - { - trigger_error( - 'UserHelper::isAuthorized() deprecated since 3.2.1. Use AuthLinkHelper::isAuthorized() instead', - E_USER_DEPRECATED - ); - - return $this->AuthLink->isAuthorized($url); - } - /** * Create links for all social providers enabled social link (connect) * * @param string $name Provider name in lowercase * @param array $provider Provider configuration * @param bool $isConnected User is connected with this provider - * * @return string */ public function socialConnectLink($name, $provider, $isConnected = false) @@ -253,15 +236,14 @@ public function socialConnectLink($name, $provider, $isConnected = false) * Create links for all social providers enabled social link (connect) * * @param array $socialAccounts All social accounts connected by a user. - * * @return string */ public function socialConnectLinkList($socialAccounts = []) { if (!Configure::read('Users.Social.login')) { - return ""; + return ''; } - $html = ""; + $html = ''; $connectedProviders = array_map( function ($item) { return strtolower($item->provider); diff --git a/src/Webauthn/AuthenticateAdapter.php b/src/Webauthn/AuthenticateAdapter.php new file mode 100644 index 000000000..592c9590a --- /dev/null +++ b/src/Webauthn/AuthenticateAdapter.php @@ -0,0 +1,68 @@ +getUserEntity(); + $allowed = array_map(function (PublicKeyCredentialSource $credential) { + return $credential->getPublicKeyCredentialDescriptor(); + }, $this->repository->findAllForUserEntity($userEntity)); + + $options = $this->server->generatePublicKeyCredentialRequestOptions( + PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_PREFERRED, // Default value + $allowed + ); + $this->request->getSession()->write( + 'Webauthn2fa.authenticateOptions', + $options + ); + + return $options; + } + + /** + * Verify the registration response + * + * @return \Webauthn\PublicKeyCredentialSource + */ + public function verifyResponse(): \Webauthn\PublicKeyCredentialSource + { + $options = $this->request->getSession()->read('Webauthn2fa.authenticateOptions'); + + return $this->loadAndCheckAssertionResponse($options); + } + + /** + * @param \Webauthn\PublicKeyCredentialRequestOptions $options request options + * @return \Webauthn\PublicKeyCredentialSource + */ + protected function loadAndCheckAssertionResponse($options): PublicKeyCredentialSource + { + return $this->server->loadAndCheckAssertionResponse( + json_encode($this->request->getData()), + $options, + $this->getUserEntity(), + $this->request + ); + } +} diff --git a/src/Webauthn/BaseAdapter.php b/src/Webauthn/BaseAdapter.php new file mode 100644 index 000000000..4ee30b09c --- /dev/null +++ b/src/Webauthn/BaseAdapter.php @@ -0,0 +1,94 @@ +request = $request; + $rpEntity = new PublicKeyCredentialRpEntity( + Configure::read('Webauthn2fa.appName'), // The application name + Configure::read('Webauthn2fa.id') + ); + /** + * @var \Cake\ORM\Entity $userSession + */ + $userSession = $request->getSession()->read('Webauthn2fa.User'); + $usersTable = $usersTable ?? TableRegistry::getTableLocator() + ->get($userSession->getSource()); + $this->user = $usersTable->get($userSession->id); + $this->repository = new UserCredentialSourceRepository( + $this->user, + $usersTable + ); + + $this->server = new Server( + $rpEntity, + $this->repository + ); + } + + /** + * @return \Webauthn\PublicKeyCredentialUserEntity + */ + protected function getUserEntity(): PublicKeyCredentialUserEntity + { + $user = $this->getUser(); + + return new PublicKeyCredentialUserEntity( + $user->webauthn_username ?? $user->username, + (string)$user->id, + (string)$user->first_name + ); + } + + /** + * @return array|mixed|null + */ + public function getUser() + { + return $this->user; + } + + /** + * @return bool + */ + public function hasCredential(): bool + { + return (bool)$this->repository->findAllForUserEntity( + $this->getUserEntity() + ); + } +} diff --git a/src/Webauthn/RegisterAdapter.php b/src/Webauthn/RegisterAdapter.php new file mode 100644 index 000000000..c28b0504a --- /dev/null +++ b/src/Webauthn/RegisterAdapter.php @@ -0,0 +1,65 @@ +getUserEntity(); + $options = $this->server->generatePublicKeyCredentialCreationOptions( + $userEntity, + PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE, + [] + ); + $this->request->getSession()->write('Webauthn2fa.registerOptions', $options); + $this->request->getSession()->write('Webauthn2fa.userEntity', $userEntity); + + return $options; + } + + /** + * Verify the registration response + * + * @return \Webauthn\PublicKeyCredentialSource + */ + public function verifyResponse(): \Webauthn\PublicKeyCredentialSource + { + $options = $this->request->getSession()->read('Webauthn2fa.registerOptions'); + $credential = $this->loadAndCheckAttestationResponse($options); + $this->repository->saveCredentialSource($credential); + + return $credential; + } + + /** + * @param \Webauthn\PublicKeyCredentialCreationOptions $options creation options + * @return \Webauthn\PublicKeyCredentialSource + */ + protected function loadAndCheckAttestationResponse($options): \Webauthn\PublicKeyCredentialSource + { + $credential = $this->server->loadAndCheckAttestationResponse( + json_encode($this->request->getData()), + $options, + $this->request + ); + + return $credential; + } +} diff --git a/src/Webauthn/Repository/UserCredentialSourceRepository.php b/src/Webauthn/Repository/UserCredentialSourceRepository.php new file mode 100644 index 000000000..ecda3cbb0 --- /dev/null +++ b/src/Webauthn/Repository/UserCredentialSourceRepository.php @@ -0,0 +1,77 @@ +user = $user; + $this->usersTable = $usersTable; + } + + /** + * @param string $publicKeyCredentialId Public key credential id + * @return \Webauthn\PublicKeyCredentialSource|null + */ + public function findOneByCredentialId(string $publicKeyCredentialId): ?PublicKeyCredentialSource + { + $encodedId = Base64Url::encode($publicKeyCredentialId); + $credential = $this->user['additional_data']['webauthn_credentials'][$encodedId] ?? null; + + return $credential + ? PublicKeyCredentialSource::createFromArray($credential) + : null; + } + + /** + * @inheritDoc + */ + public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity): array + { + if ($publicKeyCredentialUserEntity->getId() != $this->user->id) { + return []; + } + $credentials = $this->user['additional_data']['webauthn_credentials'] ?? []; + $list = []; + foreach ($credentials as $credential) { + $list[] = PublicKeyCredentialSource::createFromArray($credential); + } + + return $list; + } + + /** + * @param \Webauthn\PublicKeyCredentialSource $publicKeyCredentialSource Public key credential source + */ + public function saveCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource): void + { + $credentials = $this->user['additional_data']['webauthn_credentials'] ?? []; + $id = Base64Url::encode($publicKeyCredentialSource->getPublicKeyCredentialId()); + $credentials[$id] = json_decode(json_encode($publicKeyCredentialSource), true); + $this->user['additional_data'] = $this->user['additional_data'] ?? []; + $this->user['additional_data']['webauthn_credentials'] = $credentials; + $this->usersTable->saveOrFail($this->user); + } +} diff --git a/templates/Users/u2f_authenticate.php b/templates/Users/u2f_authenticate.php index 5b48555de..16ce1bf01 100644 --- a/templates/Users/u2f_authenticate.php +++ b/templates/Users/u2f_authenticate.php @@ -21,7 +21,7 @@ = __d('cake_d_c/users', 'Verify your registered yubico key')?> = __d('cake_d_c/users', 'Please insert and tap your yubico key')?> - + = __d('cake_d_c/users', 'When the YubiKey starts blinking, press the golden disc to activate it. Depending on the web browser you might need to confirm the use of extended information from the YubiKey.')?> = $this->Html->link( __d('cake_d_c/users', 'Reload'), diff --git a/templates/Users/webauthn2fa.php b/templates/Users/webauthn2fa.php new file mode 100644 index 000000000..3c2b7c898 --- /dev/null +++ b/templates/Users/webauthn2fa.php @@ -0,0 +1,56 @@ +Html->script('CakeDC/Users.webauthn.js', ['block' => true]); +$this->assign('title', __d('cake_d_c/users','Two-factor authentication')); +?> + + + + + + = $this->Flash->render('auth') ?> + = $this->Flash->render() ?> + + + = __d('cake_d_c/users', 'Registering your yubico key')?> + = __d('cake_d_c/users', 'Please insert and tap your yubico key')?> + = __d('cake_d_c/users', 'In order to enable your YubiKey the first step is to perform a registration.')?> + = __d('cake_d_c/users', 'When the YubiKey starts blinking, press the golden disc to activate it. Depending on the web browser you might need to confirm the use of extended information from the YubiKey.')?> + + + = __d('cake_d_c/users', 'Verify your registered yubico key')?> + = __d('cake_d_c/users', 'Please insert and tap your yubico key')?> + = __d('cake_d_c/users', 'You can now finish the authentication process using the registered device.')?> + = __d('cake_d_c/users', 'When the YubiKey starts blinking, press the golden disc to activate it. Depending on the web browser you might need to confirm the use of extended information from the YubiKey.')?> + + = $this->Html->link( + __d('cake_d_c/users','Reload'), + ['action' => 'webauthn2fa'], + ['class' => 'btn btn-primary'] + )?> + + + + + + $this->Url->build(['action' => 'webauthn2faAuthenticate']), + 'authenticateOptionsUrl' => $this->Url->build(['action' => 'webauthn2faAuthenticateOptions']), + 'registerActionUrl' => $this->Url->build(['action' => 'webauthn2faRegister']), + 'registerOptionsUrl' => $this->Url->build(['action' => 'webauthn2faRegisterOptions']), + 'isRegister' => $isRegister, + 'username' => h($username), + 'registerElemId' => 'webauthn2faRegisterInfo', + 'authenticateElemId' => 'webauthn2faAuthenticateInfo', +]; +$this->Html->scriptStart(['block' => true]); +?> +setTimeout(function() { + Webauthn2faHelper.run(= json_encode($options)?>); +}, 1000); +Html->scriptEnd();?> diff --git a/tests/Fixture/PostsFixture.php b/tests/Fixture/PostsFixture.php index 232178368..4057cbbb8 100644 --- a/tests/Fixture/PostsFixture.php +++ b/tests/Fixture/PostsFixture.php @@ -15,30 +15,9 @@ /** * PostsFixture - * */ class PostsFixture extends TestFixture { - /** - * Fields - * - * @var array - */ - // @codingStandardsIgnoreStart - public $fields = [ - 'id' => ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null], - 'title' => ['type' => 'string', 'length' => 255, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], - 'user_id' => ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], - '_constraints' => [ - 'primary' => ['type' => 'primary', 'columns' => ['id'], 'length' => []], - ], - '_options' => [ - 'engine' => 'InnoDB', - 'collation' => 'utf8_general_ci' - ], - ]; - // @codingStandardsIgnoreEnd - /** * Records * diff --git a/tests/Fixture/PostsUsersFixture.php b/tests/Fixture/PostsUsersFixture.php index 403639ee5..5f5350e9e 100644 --- a/tests/Fixture/PostsUsersFixture.php +++ b/tests/Fixture/PostsUsersFixture.php @@ -15,30 +15,9 @@ /** * PostUsers Fixture - * */ class PostsUsersFixture extends TestFixture { - /** - * Fields - * - * @var array - */ - // @codingStandardsIgnoreStart - public $fields = [ - 'id' => ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null], - 'user_id' => ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], - 'post_id' => ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], - '_constraints' => [ - 'primary' => ['type' => 'primary', 'columns' => ['id'], 'length' => []], - ], - '_options' => [ - 'engine' => 'InnoDB', - 'collation' => 'utf8_general_ci' - ], - ]; - // @codingStandardsIgnoreEnd - /** * Records * diff --git a/tests/Fixture/SocialAccountsFixture.php b/tests/Fixture/SocialAccountsFixture.php index f06ac1b6d..9789d2c45 100644 --- a/tests/Fixture/SocialAccountsFixture.php +++ b/tests/Fixture/SocialAccountsFixture.php @@ -15,42 +15,9 @@ /** * AccountsFixture - * */ class SocialAccountsFixture extends TestFixture { - /** - * Fields - * - * @var array - */ - // @codingStandardsIgnoreStart - public $fields = [ - 'id' => ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null], - 'user_id' => ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], - 'provider' => ['type' => 'string', 'length' => 255, 'unsigned' => false, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'autoIncrement' => null], - 'username' => ['type' => 'string', 'length' => 255, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], - 'reference' => ['type' => 'string', 'length' => 255, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], - 'avatar' => ['type' => 'string', 'length' => 255, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], - 'description' => ['type' => 'text', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], - 'link' => ['type' => 'text', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], - 'token' => ['type' => 'string', 'length' => 500, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], - 'token_secret' => ['type' => 'string', 'length' => 500, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], - 'token_expires' => ['type' => 'datetime', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], - 'active' => ['type' => 'boolean', 'length' => null, 'null' => false, 'default' => true, 'comment' => '', 'precision' => null], - 'data' => ['type' => 'text', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null], - 'created' => ['type' => 'datetime', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null], - 'modified' => ['type' => 'datetime', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null], - '_constraints' => [ - 'primary' => ['type' => 'primary', 'columns' => ['id'], 'length' => []], - ], - '_options' => [ - 'engine' => 'InnoDB', - 'collation' => 'utf8_general_ci' - ], - ]; - // @codingStandardsIgnoreEnd - /** * Records * diff --git a/tests/Fixture/UsersFixture.php b/tests/Fixture/UsersFixture.php index b7bb8e61f..d48f6dc55 100644 --- a/tests/Fixture/UsersFixture.php +++ b/tests/Fixture/UsersFixture.php @@ -12,50 +12,14 @@ namespace CakeDC\Users\Test\Fixture; +use Base64Url\Base64Url; use Cake\TestSuite\Fixture\TestFixture; /** * UsersFixture - * */ class UsersFixture extends TestFixture { - /** - * Fields - * - * @var array - */ - // @codingStandardsIgnoreStart - public $fields = [ - 'id' => ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null], - 'username' => ['type' => 'string', 'length' => 255, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], - 'email' => ['type' => 'string', 'length' => 255, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], - 'password' => ['type' => 'string', 'length' => 255, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], - 'first_name' => ['type' => 'string', 'length' => 50, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], - 'last_name' => ['type' => 'string', 'length' => 50, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], - 'token' => ['type' => 'string', 'length' => 255, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], - 'token_expires' => ['type' => 'datetime', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], - 'api_token' => ['type' => 'string', 'length' => 255, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], - 'activation_date' => ['type' => 'datetime', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], - 'secret' => ['type' => 'string', 'length' => 255, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], - 'secret_verified' => ['type' => 'boolean', 'length' => null, 'null' => true, 'default' => false, 'comment' => '', 'precision' => null], - 'tos_date' => ['type' => 'datetime', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], - 'active' => ['type' => 'boolean', 'length' => null, 'null' => false, 'default' => true, 'comment' => '', 'precision' => null], - 'is_superuser' => ['type' => 'boolean', 'length' => null, 'unsigned' => false, 'null' => false, 'default' => false, 'comment' => '', 'precision' => null, 'autoIncrement' => null], - 'role' => ['type' => 'string', 'length' => 255, 'null' => true, 'default' => 'user', 'comment' => '', 'precision' => null, 'fixed' => null], - 'created' => ['type' => 'datetime', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null], - 'modified' => ['type' => 'datetime', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null], - 'additional_data' => ['type' => 'text', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], - '_constraints' => [ - 'primary' => ['type' => 'primary', 'columns' => ['id'], 'length' => []], - ], - '_options' => [ - 'engine' => 'InnoDB', - 'collation' => 'utf8_general_ci' - ], - ]; - // @codingStandardsIgnoreEnd - /** * Init method * @@ -83,14 +47,31 @@ public function init(): void 'role' => 'admin', 'created' => '2015-06-24 17:33:54', 'modified' => '2015-06-24 17:33:54', - 'additional_data' => json_encode([ + 'additional_data' => [ 'u2f_registration' => [ 'keyHandle' => 'fake key handle', 'publicKey' => 'afdoaj0-23u423-ad ujsf-as8-0-afsd', 'certificate' => '23jdsfoasdj0f9sa082304823423', 'counter' => 1, ], - ]), + 'webauthn_credentials' => [ + 'MTJiMzc0ODYtOTI5OS00MzMxLWFjMzMtODViMmQ5ODViNmZl' => [ + 'publicKeyCredentialId' => '12b37486-9299-4331-ac33-85b2d985b6fe', + 'type' => 'public-key', + 'transports' => [], + 'attestationType' => 'none', + 'trustPath' => [ + 'type' => 'Webauthn\TrustPath\EmptyTrustPath', + ], + 'aaguid' => '00000000-0000-0000-0000-000000000000', + 'credentialPublicKey' => Base64Url::encode('000000000000000000000000000000000000-9999999999999999999999999999999999999999-XXXXXXXXXXXXX-ZZZZZZZZZZZ'), + 'userHandle' => Base64Url::encode('00000000-0000-0000-0000-000000000001'), + 'counter' => 190, + 'otherUI' => null, + ], + ], + ], + 'last_login' => '2015-06-24 17:33:54', ], [ 'id' => '00000000-0000-0000-0000-000000000002', @@ -112,6 +93,7 @@ public function init(): void 'role' => 'admin', 'created' => '2015-06-24 17:33:54', 'modified' => '2015-06-24 17:33:54', + 'last_login' => '2015-06-24 17:33:54', ], [ 'id' => '00000000-0000-0000-0000-000000000003', diff --git a/tests/TestCase/Authenticator/SocialAuthenticatorTest.php b/tests/TestCase/Authenticator/SocialAuthenticatorTest.php index fdc3a86fa..981f63ea2 100644 --- a/tests/TestCase/Authenticator/SocialAuthenticatorTest.php +++ b/tests/TestCase/Authenticator/SocialAuthenticatorTest.php @@ -492,6 +492,7 @@ public function testAuthenticateIdentifierReturnedNull() /** * Data provider for testAuthenticateErrorException + * * @return array */ public function dataProviderAuthenticateErrorException() @@ -513,7 +514,6 @@ public function dataProviderAuthenticateErrorException() * * @param \Exception $exception thrown exception * @param string $status expected status from Result object - * * @dataProvider dataProviderAuthenticateErrorException * @return void */ diff --git a/tests/TestCase/Authenticator/SocialPendingEmailAuthenticatorTest.php b/tests/TestCase/Authenticator/SocialPendingEmailAuthenticatorTest.php index ad6ec9f9f..949153707 100644 --- a/tests/TestCase/Authenticator/SocialPendingEmailAuthenticatorTest.php +++ b/tests/TestCase/Authenticator/SocialPendingEmailAuthenticatorTest.php @@ -61,7 +61,8 @@ public function setUp(): void */ public function testAuthenticateInvalidUrl() { - Router::connect('/users/validate-email/*', [ + $builder = Router::createRouteBuilder('/'); + $builder->connect('/users/validate-email/*', [ 'plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'socialEmail', @@ -91,7 +92,8 @@ public function testAuthenticateInvalidUrl() */ public function testAuthenticateBaseFailed() { - Router::connect('/users/social-email/*', [ + $builder = Router::createRouteBuilder('/'); + $builder->connect('/users/social-email/*', [ 'plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'socialEmail', diff --git a/tests/TestCase/Controller/Component/SetupComponentTest.php b/tests/TestCase/Controller/Component/SetupComponentTest.php index 617a8db14..d685949a2 100644 --- a/tests/TestCase/Controller/Component/SetupComponentTest.php +++ b/tests/TestCase/Controller/Component/SetupComponentTest.php @@ -21,6 +21,7 @@ /** * Class SetupComponentTest + * * @package CakeDC\Users\Test\TestCase\Controller\Component */ class SetupComponentTest extends TestCase diff --git a/tests/TestCase/Controller/Traits/BaseTraitTest.php b/tests/TestCase/Controller/Traits/BaseTraitTest.php index f97280c1a..693940e8a 100644 --- a/tests/TestCase/Controller/Traits/BaseTraitTest.php +++ b/tests/TestCase/Controller/Traits/BaseTraitTest.php @@ -30,6 +30,11 @@ use CakeDC\Users\Model\Entity\User; use PHPUnit_Framework_MockObject_RuntimeException; +/** + * Class BaseTraitTest + * + * @package CakeDC\Users\Test\TestCase\Controller\Traits + */ abstract class BaseTraitTest extends TestCase { /** @@ -56,6 +61,16 @@ abstract class BaseTraitTest extends TestCase public $loginAction = '/login-page'; + /** + * @var MockObject + */ + public $Trait; + + /** + * @var Table + */ + public $table; + /** * SetUp and create Trait * @@ -76,7 +91,7 @@ public function setUp(): void ->will($this->returnValue($this->table)); } catch (PHPUnit_Framework_MockObject_RuntimeException $ex) { debug($ex); - $this->fail("Unit tests extending BaseTraitTest should declare the trait class name in the \$traitClassName variable before calling setUp()"); + $this->fail('Unit tests extending BaseTraitTest should declare the trait class name in the $traitClassName variable before calling setUp()'); } if ($this->mockDefaultEmail) { diff --git a/tests/TestCase/Controller/Traits/Integration/LoginTraitIntegrationTest.php b/tests/TestCase/Controller/Traits/Integration/LoginTraitIntegrationTest.php index 0cf5b36cb..9bf8d74d4 100644 --- a/tests/TestCase/Controller/Traits/Integration/LoginTraitIntegrationTest.php +++ b/tests/TestCase/Controller/Traits/Integration/LoginTraitIntegrationTest.php @@ -56,7 +56,7 @@ public function testRedirectToLogin() { $this->enableRetainFlashMessages(); $this->get('/pages/home'); - $this->assertRedirect('/login?redirect=http%3A%2F%2Flocalhost%2Fpages%2Fhome'); + $this->assertRedirectContains('/login?redirect=http%3A%2F%2Flocalhost%2Fpages%2Fhome'); $this->assertFlashMessage('You are not authorized to access that location.'); } @@ -76,8 +76,8 @@ public function testLoginGetRequestNoSocialLogin() $this->assertResponseNotContains('Username or password is incorrect'); $this->assertResponseContains(''); $this->assertResponseContains('Please enter your username and password'); - $this->assertResponseContains(''); - $this->assertResponseContains(''); + $this->assertResponseContains(''); + $this->assertResponseContains(''); $this->assertResponseContains(''); $this->assertResponseContains('Login'); $this->assertResponseContains('Register'); @@ -102,8 +102,8 @@ public function testLoginGetRequest() $this->assertResponseNotContains('Username or password is incorrect'); $this->assertResponseContains(''); $this->assertResponseContains('Please enter your username and password'); - $this->assertResponseContains(''); - $this->assertResponseContains(''); + $this->assertResponseContains(''); + $this->assertResponseContains(''); $this->assertResponseContains(''); $this->assertResponseContains('Login'); $this->assertResponseContains('Register'); @@ -131,8 +131,8 @@ public function testLoginPostRequestInvalidPassword() $this->assertResponseContains('Username or password is incorrect'); $this->assertResponseContains(''); $this->assertResponseContains('Please enter your username and password'); - $this->assertResponseContains(''); - $this->assertResponseContains(''); + $this->assertResponseContains(''); + $this->assertResponseContains(''); $this->assertResponseContains(''); $this->assertResponseContains('Login'); } @@ -197,7 +197,7 @@ public function testLoginPostRequestRightPasswordIsEnabledOTP() 'username' => 'user-2', 'password' => '12345', ]); - $this->assertRedirect('/verify'); + $this->assertRedirectContains('/verify'); } /** @@ -215,7 +215,7 @@ public function testLoginPostRequestRightPasswordIsEnabledU2f() 'username' => 'user-2', 'password' => '12345', ]); - $this->assertRedirect('/users/u2f'); + $this->assertRedirectContains('/users/u2f'); } /** @@ -240,4 +240,58 @@ public function testLogoutNoUser() $this->get('/logout'); $this->assertRedirect('/login'); } + + /** + * Test redirect should not happen if the host is not defined as a known host + * + * @return void + */ + public function testRedirectAfterLoginToHostUnknown() + { + $this->post('/login?redirect=http://unknown.com/', [ + 'username' => 'user-4', + 'password' => '12345', + ]); + $this->assertRedirect('/pages/home'); + } + + /** + * Test redirect should happen for defaul localhost + * + * @return void + */ + public function testRedirectAfterLoginToAllowedHost() + { + Configure::write('Users.AllowedRedirectHosts', ['example.com']); + $this->post('/login?redirect=http://example.com/login', [ + 'username' => 'user-4', + 'password' => '12345', + ]); + // /login is authorized for this user, and example.com is in the allowed hosts + $this->assertRedirect('http://example.com/login'); + } + + public function testRedirectAfterLoginToFullBase(): void + { + $this->post('/login?redirect=http://example.com/login', [ + 'username' => 'user-4', + 'password' => '12345', + ]); + // /login is authorized for this user, and example.com is in the allowed hosts + $this->assertRedirect('http://example.com/login'); + } + + /** + * Test redirect fails if url is not allowed + * + * @return void + */ + public function testRedirectFailsIfUrlNotAllowed() + { + $this->post('/login?redirect=http://localhost/not-allowed', [ + 'username' => 'user-4', + 'password' => '12345', + ]); + $this->assertRedirect('/pages/home'); + } } diff --git a/tests/TestCase/Controller/Traits/Integration/PasswordManagementTraitIntegrationTest.php b/tests/TestCase/Controller/Traits/Integration/PasswordManagementTraitIntegrationTest.php index 55e3b09e6..47a9e337d 100644 --- a/tests/TestCase/Controller/Traits/Integration/PasswordManagementTraitIntegrationTest.php +++ b/tests/TestCase/Controller/Traits/Integration/PasswordManagementTraitIntegrationTest.php @@ -45,7 +45,7 @@ public function testRequestResetPassword() } /** - * Test login action with post request + * Test reset password workflow * * @return void */ @@ -70,7 +70,6 @@ public function testRequestResetPasswordPostValidEmail() $this->assertRedirect('/users/change-password'); $fieldName = Configure::read('Users.Key.Session.resetPasswordUserId'); - $this->assertSession($userAfter->id, $fieldName); $this->session([ $fieldName => $userAfter->id, ]); diff --git a/tests/TestCase/Controller/Traits/Integration/RegisterTraitIntegrationTest.php b/tests/TestCase/Controller/Traits/Integration/RegisterTraitIntegrationTest.php index 0866a5c84..8c7d4e852 100644 --- a/tests/TestCase/Controller/Traits/Integration/RegisterTraitIntegrationTest.php +++ b/tests/TestCase/Controller/Traits/Integration/RegisterTraitIntegrationTest.php @@ -48,7 +48,7 @@ public function testRegister() $this->assertResponseContains(''); $this->assertResponseContains(''); $this->assertResponseContains(''); - $this->assertResponseContains('Accept TOS conditions?'); + $this->assertResponseContains('Accept TOS conditions?'); $this->assertResponseContains('Submit'); } @@ -83,7 +83,7 @@ public function testRegisterPostWithErrors() $this->assertResponseContains(''); $this->assertResponseContains(''); $this->assertResponseContains(''); - $this->assertResponseContains('Accept TOS conditions?'); + $this->assertResponseContains('Accept TOS conditions?'); $this->assertResponseContains('Submit'); } diff --git a/tests/TestCase/Controller/Traits/Integration/SimpleCrudTraitIntegrationTest.php b/tests/TestCase/Controller/Traits/Integration/SimpleCrudTraitIntegrationTest.php index 54d94f0a7..9b606146e 100644 --- a/tests/TestCase/Controller/Traits/Integration/SimpleCrudTraitIntegrationTest.php +++ b/tests/TestCase/Controller/Traits/Integration/SimpleCrudTraitIntegrationTest.php @@ -93,11 +93,11 @@ public function testCrud() $this->assertRedirect('/users/index'); $this->assertFlashMessage('Password has been changed successfully'); - $this->get("/users/edit/00000000-0000-0000-0000-000000000006"); + $this->get('/users/edit/00000000-0000-0000-0000-000000000006'); $this->assertResponseContains('assertResponseContains('id="username" aria-required="true" value="user-6"'); $this->assertResponseContains('assertResponseContains('id="email" aria-required="true" value="6@example.com"'); $this->assertResponseContains('Active'); @@ -105,7 +105,7 @@ public function testCrud() $this->assertResponseContains('List Users'); $this->enableSecurityToken(); - $this->post("/users/edit/00000000-0000-0000-0000-000000000006", [ + $this->post('/users/edit/00000000-0000-0000-0000-000000000006', [ 'username' => 'my-new-username', 'email' => 'crud.email992@example.com', 'first_name' => 'Joe', @@ -113,7 +113,7 @@ public function testCrud() ]); $this->assertRedirect('/users/index'); $this->assertFlashMessage('The User has been saved'); - $this->get("/users/view/00000000-0000-0000-0000-000000000006"); + $this->get('/users/view/00000000-0000-0000-0000-000000000006'); $this->assertResponseOk(); $this->assertResponseContains('>00000000-0000-0000-0000-000000000006<'); $this->assertResponseContains('>my-new-username<'); diff --git a/tests/TestCase/Controller/Traits/LinkSocialTraitTest.php b/tests/TestCase/Controller/Traits/LinkSocialTraitTest.php index 94371c5ba..30ed9fe7b 100644 --- a/tests/TestCase/Controller/Traits/LinkSocialTraitTest.php +++ b/tests/TestCase/Controller/Traits/LinkSocialTraitTest.php @@ -18,7 +18,7 @@ use Cake\Http\Response; use Cake\Http\ServerRequest; use Cake\Http\ServerRequestFactory; -use Cake\I18n\Time; +use Cake\I18n\FrozenTime; use Cake\ORM\TableRegistry; use League\OAuth2\Client\Provider\FacebookUser; use Zend\Diactoros\Uri; @@ -274,7 +274,7 @@ public function testCallbackLinkSocialHappy() $actual = $Table->SocialAccounts->find('all')->where(['reference' => '9999911112255'])->firstOrFail(); - $expiresTime = new Time(); + $expiresTime = new FrozenTime(); $tokenExpires = $expiresTime->setTimestamp($Token->getExpires())->format('Y-m-d H:i:s'); $expected = [ diff --git a/tests/TestCase/Controller/Traits/LoginTraitTest.php b/tests/TestCase/Controller/Traits/LoginTraitTest.php index 66cd4707c..79d0a860b 100644 --- a/tests/TestCase/Controller/Traits/LoginTraitTest.php +++ b/tests/TestCase/Controller/Traits/LoginTraitTest.php @@ -92,6 +92,8 @@ public function testLoginHappy() $user = $this->Trait->getUsersTable()->get('00000000-0000-0000-0000-000000000002'); $passwordBefore = $user['password']; $this->assertNotEmpty($passwordBefore); + $lastLoginBefore = $user['last_login']; + $this->assertNotEmpty($lastLoginBefore); $this->_mockAuthentication($user->toArray(), $failures); $this->Trait->Flash->expects($this->never()) ->method('error'); @@ -130,6 +132,10 @@ public function testLoginHappy() $userAfter = $this->Trait->getUsersTable()->get('00000000-0000-0000-0000-000000000002'); $passwordAfter = $userAfter['password']; $this->assertSame($passwordBefore, $passwordAfter); + $lastLoginAfter = $userAfter['last_login']; + $this->assertNotEmpty($lastLoginAfter); + $now = \Cake\I18n\FrozenTime::now(); + $this->assertEqualsWithDelta($lastLoginAfter->timestamp, $now->timestamp, 2); } /** diff --git a/tests/TestCase/Controller/Traits/OneTimePasswordVerifyTraitTest.php b/tests/TestCase/Controller/Traits/OneTimePasswordVerifyTraitTest.php index 168d137da..683bfc0c7 100644 --- a/tests/TestCase/Controller/Traits/OneTimePasswordVerifyTraitTest.php +++ b/tests/TestCase/Controller/Traits/OneTimePasswordVerifyTraitTest.php @@ -59,7 +59,6 @@ public function tearDown(): void /** * testVerifyHappy - * */ public function testVerifyHappy() { @@ -87,7 +86,6 @@ public function testVerifyHappy() /** * testVerifyHappy - * */ public function testVerifyNotEnabled() { @@ -106,7 +104,6 @@ public function testVerifyNotEnabled() /** * testVerifyHappy - * */ public function testVerifyGetShowQR() { @@ -136,10 +133,10 @@ public function testVerifyGetShowQR() ->method('is') ->with('post') ->will($this->returnValue(false)); - $this->Trait->OneTimePasswordAuthenticator->expects($this->at(0)) + $this->Trait->OneTimePasswordAuthenticator->expects($this->once()) ->method('createSecret') ->will($this->returnValue('newSecret')); - $this->Trait->OneTimePasswordAuthenticator->expects($this->at(1)) + $this->Trait->OneTimePasswordAuthenticator->expects($this->once()) ->method('getQRCodeImageAsDataUri') ->with('email@example.com', 'newSecret') ->will($this->returnValue('newDataUriGenerated')); @@ -178,11 +175,11 @@ public function testVerifyGetGeneratesNewSecret() ->will($this->returnValue(false)); $this->Trait->OneTimePasswordAuthenticator - ->expects($this->at(0)) + ->expects($this->once()) ->method('createSecret') ->will($this->returnValue('newSecret')); $this->Trait->OneTimePasswordAuthenticator - ->expects($this->at(1)) + ->expects($this->once()) ->method('getQRCodeImageAsDataUri') ->with('email@example.com', 'newSecret') ->will($this->returnValue('newDataUriGenerated')); @@ -241,7 +238,7 @@ public function testVerifyGetDoesNotGenerateNewSecret() ->expects($this->never()) ->method('createSecret'); $this->Trait->OneTimePasswordAuthenticator - ->expects($this->at(0)) + ->expects($this->once()) ->method('getQRCodeImageAsDataUri') ->with('email@example.com', 'alreadyPresentSecret') ->will($this->returnValue('newDataUriGenerated')); diff --git a/tests/TestCase/Controller/Traits/PasswordManagementTraitTest.php b/tests/TestCase/Controller/Traits/PasswordManagementTraitTest.php index 9c75ef05b..9a3d8a6ad 100644 --- a/tests/TestCase/Controller/Traits/PasswordManagementTraitTest.php +++ b/tests/TestCase/Controller/Traits/PasswordManagementTraitTest.php @@ -411,7 +411,6 @@ public function testRequestPasswordEmptyReference() /** * @dataProvider ensureUserActiveForResetPasswordFeature - * * @return void */ public function testEnsureUserActiveForResetPasswordFeature($ensureActive) @@ -451,7 +450,6 @@ public function ensureUserActiveForResetPasswordFeature() /** * @dataProvider ensureOneTimePasswordAuthenticatorResets - * * @return void */ public function testRequestGoogleAuthTokenResetWithValidUser($userId, $entityId, $method, $msg) diff --git a/tests/TestCase/Controller/Traits/RegisterTraitTest.php b/tests/TestCase/Controller/Traits/RegisterTraitTest.php index fcb08a03f..1ee7df055 100644 --- a/tests/TestCase/Controller/Traits/RegisterTraitTest.php +++ b/tests/TestCase/Controller/Traits/RegisterTraitTest.php @@ -64,11 +64,12 @@ public function testValidateEmail() */ public function testRegister() { - Router::connect('/users/validate-email/*', [ - 'plugin' => 'CakeDC/Users', - 'controller' => 'Users', - 'action' => 'validateEmail', - ]); + $builder = Router::createRouteBuilder('/'); + $builder->connect('/users/validate-email/*', [ + 'plugin' => 'CakeDC/Users', + 'controller' => 'Users', + 'action' => 'validateEmail', + ]); $this->assertEquals(0, $this->table->find()->where(['username' => 'testRegistration'])->count()); $this->_mockRequestPost(); @@ -127,11 +128,12 @@ public function testRegisterWithEventFalseResult() */ public function testRegisterWithEventSuccessResult() { - Router::connect('/users/validate-email/*', [ - 'plugin' => 'CakeDC/Users', - 'controller' => 'Users', - 'action' => 'validateEmail', - ]); + $builder = Router::createRouteBuilder('/'); + $builder->connect('/users/validate-email/*', [ + 'plugin' => 'CakeDC/Users', + 'controller' => 'Users', + 'action' => 'validateEmail', + ]); $data = [ 'username' => 'testRegistration', @@ -169,11 +171,12 @@ public function testRegisterWithEventSuccessResult() */ public function testRegisterReCaptcha() { - Router::connect('/users/validate-email/*', [ - 'plugin' => 'CakeDC/Users', - 'controller' => 'Users', - 'action' => 'validateEmail', - ]); + $builder = Router::createRouteBuilder('/'); + $builder->connect('/users/validate-email/*', [ + 'plugin' => 'CakeDC/Users', + 'controller' => 'Users', + 'action' => 'validateEmail', + ]); Configure::write('Users.reCaptcha.registration', true); $this->assertEquals(0, $this->table->find()->where(['username' => 'testRegistration'])->count()); @@ -190,7 +193,7 @@ public function testRegisterReCaptcha() $this->Trait->expects($this->once()) ->method('redirect') ->with(['action' => 'login']); - $this->Trait->getRequest()->expects($this->at(0)) + $this->Trait->getRequest()->expects($this->any()) ->method('getData') ->with() ->will($this->returnValue([ @@ -227,7 +230,7 @@ public function testRegisterValidationErrors() ->will($this->returnValue(true)); $this->Trait->expects($this->never()) ->method('redirect'); - $this->Trait->getRequest()->expects($this->at(0)) + $this->Trait->getRequest()->expects($this->any()) ->method('getData') ->with() ->will($this->returnValue([ @@ -259,10 +262,10 @@ public function testRegisterRecaptchaNotValid() $this->Trait->Flash->expects($this->once()) ->method('error') ->with('Invalid reCaptcha'); - $this->Trait->expects($this->once()) + $this->Trait->expects($this->any()) ->method('validateRecaptcha') ->will($this->returnValue(false)); - $this->Trait->getRequest()->expects($this->at(0)) + $this->Trait->getRequest()->expects($this->any()) ->method('getData') ->with() ->will($this->returnValue([ @@ -308,11 +311,12 @@ public function testRegisterGet() */ public function testRegisterRecaptchaDisabled() { - Router::connect('/users/validate-email/*', [ - 'plugin' => 'CakeDC/Users', - 'controller' => 'Users', - 'action' => 'validateEmail', - ]); + $builder = Router::createRouteBuilder('/'); + $builder->connect('/users/validate-email/*', [ + 'plugin' => 'CakeDC/Users', + 'controller' => 'Users', + 'action' => 'validateEmail', + ]); Configure::write('Users.Registration.reCaptcha', false); $this->assertEquals(0, $this->table->find()->where(['username' => 'testRegistration'])->count()); @@ -328,7 +332,7 @@ public function testRegisterRecaptchaDisabled() $this->Trait->expects($this->once()) ->method('redirect') ->with(['action' => 'login']); - $this->Trait->getRequest()->expects($this->at(0)) + $this->Trait->getRequest()->expects($this->once()) ->method('getData') ->with() ->will($this->returnValue([ @@ -367,11 +371,12 @@ public function testRegisterNotEnabled() */ public function testRegisterLoggedInUserAllowed() { - Router::connect('/users/validate-email/*', [ - 'plugin' => 'CakeDC/Users', - 'controller' => 'Users', - 'action' => 'validateEmail', - ]); + $builder = Router::createRouteBuilder('/'); + $builder->connect('/users/validate-email/*', [ + 'plugin' => 'CakeDC/Users', + 'controller' => 'Users', + 'action' => 'validateEmail', + ]); Configure::write('Users.Registration.allowLoggedIn', true); $this->assertEquals(0, $this->table->find()->where(['username' => 'testRegistration'])->count()); @@ -385,7 +390,7 @@ public function testRegisterLoggedInUserAllowed() $this->Trait->expects($this->once()) ->method('redirect') ->with(['action' => 'login']); - $this->Trait->getRequest()->expects($this->at(0)) + $this->Trait->getRequest()->expects($this->once()) ->method('getData') ->with() ->will($this->returnValue([ @@ -426,4 +431,67 @@ public function testRegisterLoggedInUserNotAllowed() $this->Trait->register(); } + + /** + * test + * + * @return void + */ + public function testNotShowingVerboseErrorOnRegisterWithDefaultConfig() + { + //register user and not validate the email + $this->testRegister(); + + $this->_mockRequestPost(); + $this->_mockAuthentication(); + $this->_mockFlash(); + $this->_mockDispatchEvent(); + $this->Trait->Flash->expects($this->once()) + ->method('error') + ->with('The user could not be saved'); + + $this->Trait->getRequest()->expects($this->once()) + ->method('getData') + ->will($this->returnValue([ + 'username' => 'testRegistration', + 'password' => 'password', + 'email' => 'test-registration@example.com', + 'password_confirm' => 'password', + 'tos' => 1, + ])); + + $this->Trait->register(); + } + + /** + * test + * + * @return void + */ + public function testShowingVerboseErrorOnRegisterWithUpdatedConfig() + { + //register user and not validate the email + $this->testRegister(); + + $this->_mockRequestPost(); + $this->_mockAuthentication(); + $this->_mockFlash(); + $this->_mockDispatchEvent(); + + $this->Trait->Flash->expects($this->once()) + ->method('error') + ->with('Email already exists'); + + $this->Trait->getRequest()->expects($this->once()) + ->method('getData') + ->will($this->returnValue([ + 'username' => 'testRegistration1', + 'password' => 'password', + 'email' => 'test-registration@example.com', + 'password_confirm' => 'password', + 'tos' => 1, + ])); + Configure::write('Users.Registration.showVerboseError', true); + $this->Trait->register(); + } } diff --git a/tests/TestCase/Controller/Traits/SimpleCrudTraitTest.php b/tests/TestCase/Controller/Traits/SimpleCrudTraitTest.php index 9351a769c..dc792ee49 100644 --- a/tests/TestCase/Controller/Traits/SimpleCrudTraitTest.php +++ b/tests/TestCase/Controller/Traits/SimpleCrudTraitTest.php @@ -16,6 +16,11 @@ use Cake\Datasource\Exception\InvalidPrimaryKeyException; use Cake\Datasource\Exception\RecordNotFoundException; +/** + * Class SimpleCrudTraitTest + * + * @package CakeDC\Users\Test\TestCase\Controller\Traits + */ class SimpleCrudTraitTest extends BaseTraitTest { public $viewVars; @@ -127,7 +132,7 @@ public function testAddGet() $this->_mockRequestGet(); $this->Trait->add(); $expected = [ - 'Users' => $this->table->newEntity([]), + 'Users' => $this->table->newEmptyEntity(), 'tableAlias' => 'Users', '_serialize' => [ 'Users', @@ -147,11 +152,11 @@ public function testAddPostHappy() $this->assertSame(0, $this->table->find()->where(['username' => 'testuser'])->count()); $this->_mockRequestPost(); $this->_mockFlash(); - $this->Trait->getRequest()->expects($this->at(0)) + $this->Trait->getRequest()->expects($this->once()) ->method('is') ->with('post') ->will($this->returnValue(true)); - $this->Trait->getRequest()->expects($this->at(1)) + $this->Trait->getRequest()->expects($this->once()) ->method('getData') ->with() ->will($this->returnValue([ @@ -180,11 +185,11 @@ public function testAddPostErrors() $this->assertSame(0, $this->table->find()->where(['username' => 'testuser'])->count()); $this->_mockRequestPost(); $this->_mockFlash(); - $this->Trait->getRequest()->expects($this->at(0)) + $this->Trait->getRequest()->expects($this->once()) ->method('is') ->with('post') ->will($this->returnValue(true)); - $this->Trait->getRequest()->expects($this->at(1)) + $this->Trait->getRequest()->expects($this->once()) ->method('getData') ->with() ->will($this->returnValue([ @@ -213,11 +218,11 @@ public function testEditPostHappy() $this->assertEquals('user-1@test.com', $this->table->get('00000000-0000-0000-0000-000000000001')->email); $this->_mockRequestPost(['patch', 'post', 'put']); $this->_mockFlash(); - $this->Trait->getRequest()->expects($this->at(0)) + $this->Trait->getRequest()->expects($this->once()) ->method('is') ->with(['patch', 'post', 'put']) ->will($this->returnValue(true)); - $this->Trait->getRequest()->expects($this->at(1)) + $this->Trait->getRequest()->expects($this->once()) ->method('getData') ->with() ->will($this->returnValue([ @@ -243,11 +248,11 @@ public function testEditPostErrors() $this->assertEquals('user-1@test.com', $this->table->get('00000000-0000-0000-0000-000000000001')->email); $this->_mockRequestPost(['patch', 'post', 'put']); $this->_mockFlash(); - $this->Trait->getRequest()->expects($this->at(0)) + $this->Trait->getRequest()->expects($this->once()) ->method('is') ->with(['patch', 'post', 'put']) ->will($this->returnValue(true)); - $this->Trait->getRequest()->expects($this->at(1)) + $this->Trait->getRequest()->expects($this->once()) ->method('getData') ->with() ->will($this->returnValue([ diff --git a/tests/TestCase/Controller/Traits/U2fTraitTest.php b/tests/TestCase/Controller/Traits/U2fTraitTest.php index 7d2229983..6d7db567e 100644 --- a/tests/TestCase/Controller/Traits/U2fTraitTest.php +++ b/tests/TestCase/Controller/Traits/U2fTraitTest.php @@ -94,14 +94,14 @@ public function dataProviderU2User() 'id' => '00000000-0000-0000-0000-000000000001', 'username' => 'user-1', ]); - $withWhoutRegistration = new User([ + $withoutRegistration = new User([ 'id' => '00000000-0000-0000-0000-000000000002', 'username' => 'user-2', ]); return [ - [$empty, ['action' => 'login']], - [$withWhoutRegistration, ['action' => 'u2fRegister']], + // [$empty, ['action' => 'login']], + // [$withoutRegistration, ['action' => 'u2fRegister']], [$withRegistration, ['action' => 'u2fAuthenticate']], ]; } @@ -111,7 +111,6 @@ public function dataProviderU2User() * * @param array $userData session user data * @param mixed $redirect expetected redirect - * * @dataProvider dataProviderU2User * @return void */ @@ -163,7 +162,7 @@ public function testU2fRegisterOkay() ->setMethods(['getRegisterData']) ->getMock(); - $registerRequest = new RegisterRequest("sample chalange", "https://localhost"); + $registerRequest = new RegisterRequest('sample chalange', 'https://localhost'); $signs = [ ['fake' => new \stdClass()], ['fake2' => new \stdClass()], @@ -223,7 +222,6 @@ public function dataProviderU2fRegisterRedirect() * * @param array $userData session user data * @param mixed $redirect expetected redirect - * * @dataProvider dataProviderU2fRegisterRedirect * @return void */ @@ -285,7 +283,7 @@ public function testU2fRegisterFinishOkay() ->setMethods(['doRegister']) ->getMock(); - $registerRequest = new RegisterRequest("sample chalange", "https://localhost"); + $registerRequest = new RegisterRequest('sample chalange', 'https://localhost'); $registerRequest = json_decode(json_encode($registerRequest)); $signs = [ ['fake' => new \stdClass()], @@ -296,9 +294,9 @@ public function testU2fRegisterFinishOkay() 'fakeB' => 'fakevalueb', ])); $registration = new Registration(); - $registration->certificate = "user registration cert " . time(); + $registration->certificate = 'user registration cert ' . time(); $registration->counter = 1; - $registration->publicKey = "pub skska08u90234230990"; + $registration->publicKey = 'pub skska08u90234230990'; $registration->keyHandle = 'hahdofa02390423udu9ma0dumfá0dsufm2um9432uu903u923'; $this->Trait->getRequest()->expects($this->once()) @@ -355,9 +353,9 @@ public function testU2fRegisterFinishOkay() $this->assertEquals(json_encode($registration), json_encode($savedRegistration)); $registration = new Registration(); - $registration->certificate = "user registration cert " . time(); + $registration->certificate = 'user registration cert ' . time(); $registration->counter = 1; - $registration->publicKey = "pub skska08u90234230990"; + $registration->publicKey = 'pub skska08u90234230990'; $registration->keyHandle = 'hahdofa02390423udu9ma0dumfá0dsufm2um9432uu903u923'; } @@ -383,16 +381,16 @@ public function testU2fRegisterFinishException() ->setMethods(['doRegister']) ->getMock(); - $registerRequest = new RegisterRequest("sample chalange", "https://localhost"); + $registerRequest = new RegisterRequest('sample chalange', 'https://localhost'); $registerRequest = json_decode(json_encode($registerRequest)); $registerResponse = json_decode(json_encode([ 'fakeA' => 'fakevaluea', 'fakeB' => 'fakevalueb', ])); $registration = new Registration(); - $registration->certificate = "user registration cert " . time(); + $registration->certificate = 'user registration cert ' . time(); $registration->counter = 1; - $registration->publicKey = "pub skska08u90234230990"; + $registration->publicKey = 'pub skska08u90234230990'; $registration->keyHandle = 'hahdofa02390423udu9ma0dumfá0dsufm2um9432uu903u923'; $this->Trait->getRequest()->expects($this->once()) @@ -453,9 +451,9 @@ public function testU2fRegisterFinishException() $this->assertNull($savedRegistration); $registration = new Registration(); - $registration->certificate = "user registration cert " . time(); + $registration->certificate = 'user registration cert ' . time(); $registration->counter = 1; - $registration->publicKey = "pub skska08u90234230990"; + $registration->publicKey = 'pub skska08u90234230990'; $registration->keyHandle = 'hahdofa02390423udu9ma0dumfá0dsufm2um9432uu903u923'; } @@ -483,7 +481,6 @@ public function dataProviderU2fAuthenticateRedirectCustomUser() * * @param array $userData session user data * @param mixed $redirect expetected redirect - * * @dataProvider dataProviderU2fAuthenticateRedirectCustomUser * @return void */ diff --git a/tests/TestCase/Controller/Traits/UserValidationTraitTest.php b/tests/TestCase/Controller/Traits/UserValidationTraitTest.php index d34a6a726..584d8596c 100644 --- a/tests/TestCase/Controller/Traits/UserValidationTraitTest.php +++ b/tests/TestCase/Controller/Traits/UserValidationTraitTest.php @@ -56,6 +56,26 @@ public function testValidateHappyEmail() $this->assertTrue($user->active); } + /** + * test + * + * @return void + */ + public function testValidateHappyEmailWithAfterEmailTokenValidationEvent() + { + $event = new Event('event'); + $event->setResult([ + 'action' => 'newAction', + ]); + $this->Trait->expects($this->once()) + ->method('dispatchEvent') + ->will($this->returnValue($event)); + $this->Trait->expects($this->once()) + ->method('redirect') + ->with(['action' => 'newAction']); + $this->Trait->validate('email', 'token-3'); + } + /** * test * diff --git a/tests/TestCase/Controller/Traits/Webauthn2faTraitTest.php b/tests/TestCase/Controller/Traits/Webauthn2faTraitTest.php new file mode 100644 index 000000000..e47f2c661 --- /dev/null +++ b/tests/TestCase/Controller/Traits/Webauthn2faTraitTest.php @@ -0,0 +1,493 @@ +traitClassName = 'CakeDC\Users\Controller\UsersController'; + $this->traitMockMethods = ['dispatchEvent', 'isStopped', 'redirect', 'getUsersTable', 'set', 'createU2fLib', 'getData', 'getU2fAuthenticationChecker']; + + parent::setUp(); + + $request = new ServerRequest(); + $this->Trait->setRequest($request); + Configure::write('Webauthn2fa.enabled', true); + Configure::write('Webauthn2fa.appName', 'ACME Webauthn Server'); + Configure::write('Webauthn2fa.id', 'localhost'); + } + + /** + * Mock session and mock session attributes + * + * @return \Cake\Http\Session + */ + protected function _mockSession($attributes) + { + $session = new \Cake\Http\Session(); + + foreach ($attributes as $field => $value) { + $session->write($field, $value); + } + + $this->Trait + ->getRequest() + ->expects($this->any()) + ->method('getSession') + ->willReturn($session); + + return $session; + } + + /** + * Test webauthn2fa method when requires register + * + * @return void + */ + public function testWebauthn2faIsRegister() + { + $request = $this->getMockBuilder('Cake\Http\ServerRequest') + ->setMethods(['getSession', 'is']) + ->getMock(); + $this->Trait->setRequest($request); + $request->expects($this->any()) + ->method('is') + ->with( + $this->equalTo('ssl') + )->will($this->returnValue(true)); + + $table = TableRegistry::getTableLocator() + ->get('CakeDC/Users.Users'); + $user = $table->get('00000000-0000-0000-0000-000000000002'); + $this->_mockSession([ + 'Webauthn2fa.User' => $user, + ]); + $this->Trait + ->expects($this->exactly(2)) + ->method('set') + ->withConsecutive(['isRegister', true], ['username', 'user-2']); + $this->Trait->webauthn2fa(); + $this->assertSame( + $user, + $this->Trait->getRequest()->getSession()->read('Webauthn2fa.User') + ); + } + + /** + * Test webauthn2fa method when DON'T require register + * + * @return void + */ + public function testWebauthn2faDontRequireRegister() + { + $request = $this->getMockBuilder('Cake\Http\ServerRequest') + ->setMethods(['getSession', 'is']) + ->getMock(); + $this->Trait->setRequest($request); + $request->expects($this->any()) + ->method('is') + ->with( + $this->equalTo('ssl') + )->will($this->returnValue(true)); + + $table = TableRegistry::getTableLocator() + ->get('CakeDC/Users.Users'); + $user = $table->get('00000000-0000-0000-0000-000000000001'); + $this->_mockSession([ + 'Webauthn2fa.User' => $user, + ]); + $this->Trait + ->expects($this->exactly(2)) + ->method('set') + ->withConsecutive(['isRegister', false], ['username', 'user-1']); + $this->Trait->webauthn2fa(); + $this->assertSame( + $user, + $this->Trait->getRequest()->getSession()->read('Webauthn2fa.User') + ); + } + + /** + * Test webauthn2faRegisterOptions method when DON'T require register + * + * @return void + */ + public function testWebauthn2faRegisterOptionsDontRequireRegister() + { + $request = $this->getMockBuilder('Cake\Http\ServerRequest') + ->setMethods(['getSession', 'is']) + ->getMock(); + $this->Trait->setRequest($request); + $request->expects($this->any()) + ->method('is') + ->with( + $this->equalTo('ssl') + )->will($this->returnValue(true)); + + $table = TableRegistry::getTableLocator() + ->get('CakeDC/Users.Users'); + $user = $table->get('00000000-0000-0000-0000-000000000001'); + $this->_mockSession([ + 'Webauthn2fa.User' => $user, + ]); + $this->expectException(BadRequestException::class); + $this->expectExceptionMessage('User already has configured webauthn2fa'); + $this->Trait->webauthn2faRegisterOptions(); + } + + /** + * Test webauthn2faRegisterOptions method + * + * @return void + */ + public function testWebauthn2faRegisterOptions() + { + $request = $this->getMockBuilder('Cake\Http\ServerRequest') + ->setMethods(['getSession', 'is']) + ->getMock(); + $this->Trait->setRequest($request); + $request->expects($this->any()) + ->method('is') + ->with( + $this->equalTo('ssl') + )->will($this->returnValue(true)); + + $table = TableRegistry::getTableLocator() + ->get('CakeDC/Users.Users'); + $user = $table->get('00000000-0000-0000-0000-000000000002'); + $this->_mockSession([ + 'Webauthn2fa.User' => $user, + ]); + + $response = $this->Trait->webauthn2faRegisterOptions(); + $data = json_decode((string)$response->getBody(), true); + $this->assertArrayHasKey('rp', $data); + $this->assertArrayHasKey('user', $data); + $this->assertSame('user-2', $data['user']['name']); + $this->assertSame( + $user, + $this->Trait->getRequest()->getSession()->read('Webauthn2fa.User') + ); + } + + /** + * Test webauthn2faRegister method when DON'T require register + * + * @return void + */ + public function testWebauthn2faRegisterDontRequireRegister() + { + $request = $this->getMockBuilder('Cake\Http\ServerRequest') + ->setMethods(['getSession', 'is']) + ->getMock(); + $this->Trait->setRequest($request); + $request->expects($this->any()) + ->method('is') + ->with( + $this->equalTo('ssl') + )->will($this->returnValue(true)); + + $table = TableRegistry::getTableLocator() + ->get('CakeDC/Users.Users'); + $user = $table->get('00000000-0000-0000-0000-000000000001'); + $this->_mockSession([ + 'Webauthn2fa.User' => $user, + ]); + $this->expectException(BadRequestException::class); + $this->expectExceptionMessage('User already has configured webauthn2fa'); + $this->Trait->webauthn2faRegister(); + } + + /** + * Test webauthn2faRegisterOptions method + * + * @return void + */ + public function testWebauthn2faRegister() + { + $table = TableRegistry::getTableLocator() + ->get('CakeDC/Users.Users'); + $user = $table->get('00000000-0000-0000-0000-000000000002'); + $request = ServerRequestFactory::fromGlobals(); + $request->getSession()->write('Webauthn2fa.User', $user); + + $this->Trait->setRequest($request); + $adapter = $this->getMockBuilder(RegisterAdapter::class) + ->onlyMethods(['verifyResponse']) + ->setConstructorArgs([$request]) + ->getMock(); + $publicKeyCredentialId = '12b37486-9299-4331-ac33-85b2d985b6fe'; + $userId = '00000000-0000-0000-0000-000000000002'; + $credentialData = [ + 'publicKeyCredentialId' => $publicKeyCredentialId, + 'type' => 'public-key', + 'transports' => [], + 'attestationType' => 'none', + 'trustPath' => [ + 'type' => 'Webauthn\TrustPath\EmptyTrustPath', + ], + 'aaguid' => '00000000-0000-0000-0000-000000000000', + 'credentialPublicKey' => Base64Url::encode('000000000000000000000000000000000000-9999999999999999999999999999999999999999-XXXXXXXXXXXXX-YYYYYYYYYYY'), + 'userHandle' => Base64Url::encode($userId), + 'counter' => 191, + 'otherUI' => null, + ]; + $credential = PublicKeyCredentialSource::createFromArray($credentialData); + $adapter->expects($this->once()) + ->method('verifyResponse') + ->willReturn($credential); + + $traitMockMethods = array_unique(array_merge(['getUsersTable', 'getWebauthn2faRegisterAdapter'], $this->traitMockMethods)); + $this->Trait = $this->getMockBuilder($this->traitClassName) + ->setMethods($traitMockMethods) + ->getMock(); + $this->Trait->expects($this->once()) + ->method('getWebauthn2faRegisterAdapter') + ->willReturn($adapter); + $this->Trait->expects($this->any()) + ->method('getUsersTable') + ->will($this->returnValue($this->table)); + $data = '{"id":"LFdoCFJTyB82ZzSJUHc-c72yraRc_1mPvGX8ToE8su39xX26Jcqd31LUkKOS36FIAWgWl6itMKqmDvruha6ywA","rawId":"LFdoCFJTyB82ZzSJUHc-c72yraRc_1mPvGX8ToE8su39xX26Jcqd31LUkKOS36FIAWgWl6itMKqmDvruha6ywA","response":{"clientDataJSON":"eyJjaGFsbGVuZ2UiOiJOeHlab3B3VktiRmw3RW5uTWFlXzVGbmlyN1FKN1FXcDFVRlVLakZIbGZrIiwiY2xpZW50RXh0ZW5zaW9ucyI6e30sImhhc2hBbGdvcml0aG0iOiJTSEEtMjU2Iiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwIiwidHlwZSI6IndlYmF1dGhuLmNyZWF0ZSJ9","attestationObject":"o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEcwRQIgVzzvX3Nyp_g9j9f2B-tPWy6puW01aZHI8RXjwqfDjtQCIQDLsdniGPO9iKr7tdgVV-FnBYhvzlZLG3u28rVt10YXfGN4NWOBWQJOMIICSjCCATKgAwIBAgIEVxb3wDANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowLDEqMCgGA1UEAwwhWXViaWNvIFUyRiBFRSBTZXJpYWwgMjUwNTY5MjI2MTc2MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZNkcVNbZV43TsGB4TEY21UijmDqvNSfO6y3G4ytnnjP86ehjFK28-FdSGy9MSZ-Ur3BVZb4iGVsptk5NrQ3QYqM7MDkwIgYJKwYBBAGCxAoCBBUxLjMuNi4xLjQuMS40MTQ4Mi4xLjUwEwYLKwYBBAGC5RwCAQEEBAMCBSAwDQYJKoZIhvcNAQELBQADggEBAHibGMqbpNt2IOL4i4z96VEmbSoid9Xj--m2jJqg6RpqSOp1TO8L3lmEA22uf4uj_eZLUXYEw6EbLm11TUo3Ge-odpMPoODzBj9aTKC8oDFPfwWj6l1O3ZHTSma1XVyPqG4A579f3YAjfrPbgj404xJns0mqx5wkpxKlnoBKqo1rqSUmonencd4xanO_PHEfxU0iZif615Xk9E4bcANPCfz-OLfeKXiT-1msixwzz8XGvl2OTMJ_Sh9G9vhE-HjAcovcHfumcdoQh_WM445Za6Pyn9BZQV3FCqMviRR809sIATfU5lu86wu_5UGIGI7MFDEYeVGSqzpzh6mlcn8QSIZoYXV0aERhdGFYxEmWDeWIDoxodDQXD2R2YFuP5K65ooYyx5lc87qDHZdjQQAAAAAAAAAAAAAAAAAAAAAAAAAAAEAsV2gIUlPIHzZnNIlQdz5zvbKtpFz_WY-8ZfxOgTyy7f3Ffbolyp3fUtSQo5LfoUgBaBaXqK0wqqYO-u6FrrLApQECAyYgASFYIPr9-YH8DuBsOnaI3KJa0a39hyxh9LDtHErNvfQSyxQsIlgg4rAuQQ5uy4VXGFbkiAt0uwgJJodp-DymkoBcrGsLtkI"},"type":"public-key"}'; + $request = $request->withParsedBody( + json_decode($data, true) + ); + $this->Trait->setRequest($request); + $response = $this->Trait->webauthn2faRegister(); + $this->assertEquals('{"success":true}', (string)$response->getBody()); + } + + /** + * Test webauthn2faRegisterOptions method + * + * @return void + */ + public function testWebauthn2faRegisterError() + { + $table = TableRegistry::getTableLocator() + ->get('CakeDC/Users.Users'); + $user = $table->get('00000000-0000-0000-0000-000000000002'); + $request = ServerRequestFactory::fromGlobals(); + $request->getSession()->write('Webauthn2fa.User', $user); + + $this->Trait->setRequest($request); + $adapter = $this->getMockBuilder(RegisterAdapter::class) + ->onlyMethods(['verifyResponse']) + ->setConstructorArgs([$request]) + ->getMock(); + + $adapter->expects($this->once()) + ->method('verifyResponse') + ->willThrowException(new \Exception('Testing error exception for webauthn2faRegister')); + + $traitMockMethods = array_unique(array_merge(['getUsersTable', 'getWebauthn2faRegisterAdapter'], $this->traitMockMethods)); + $this->Trait = $this->getMockBuilder($this->traitClassName) + ->setMethods($traitMockMethods) + ->getMock(); + $this->Trait->expects($this->once()) + ->method('getWebauthn2faRegisterAdapter') + ->willReturn($adapter); + $this->Trait->expects($this->any()) + ->method('getUsersTable') + ->will($this->returnValue($this->table)); + $data = '{"id":"LFdoCFJTyB82ZzSJUHc-c72yraRc_1mPvGX8ToE8su39xX26Jcqd31LUkKOS36FIAWgWl6itMKqmDvruha6ywA","rawId":"LFdoCFJTyB82ZzSJUHc-c72yraRc_1mPvGX8ToE8su39xX26Jcqd31LUkKOS36FIAWgWl6itMKqmDvruha6ywA","response":{"clientDataJSON":"eyJjaGFsbGVuZ2UiOiJOeHlab3B3VktiRmw3RW5uTWFlXzVGbmlyN1FKN1FXcDFVRlVLakZIbGZrIiwiY2xpZW50RXh0ZW5zaW9ucyI6e30sImhhc2hBbGdvcml0aG0iOiJTSEEtMjU2Iiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwIiwidHlwZSI6IndlYmF1dGhuLmNyZWF0ZSJ9","attestationObject":"o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEcwRQIgVzzvX3Nyp_g9j9f2B-tPWy6puW01aZHI8RXjwqfDjtQCIQDLsdniGPO9iKr7tdgVV-FnBYhvzlZLG3u28rVt10YXfGN4NWOBWQJOMIICSjCCATKgAwIBAgIEVxb3wDANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowLDEqMCgGA1UEAwwhWXViaWNvIFUyRiBFRSBTZXJpYWwgMjUwNTY5MjI2MTc2MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZNkcVNbZV43TsGB4TEY21UijmDqvNSfO6y3G4ytnnjP86ehjFK28-FdSGy9MSZ-Ur3BVZb4iGVsptk5NrQ3QYqM7MDkwIgYJKwYBBAGCxAoCBBUxLjMuNi4xLjQuMS40MTQ4Mi4xLjUwEwYLKwYBBAGC5RwCAQEEBAMCBSAwDQYJKoZIhvcNAQELBQADggEBAHibGMqbpNt2IOL4i4z96VEmbSoid9Xj--m2jJqg6RpqSOp1TO8L3lmEA22uf4uj_eZLUXYEw6EbLm11TUo3Ge-odpMPoODzBj9aTKC8oDFPfwWj6l1O3ZHTSma1XVyPqG4A579f3YAjfrPbgj404xJns0mqx5wkpxKlnoBKqo1rqSUmonencd4xanO_PHEfxU0iZif615Xk9E4bcANPCfz-OLfeKXiT-1msixwzz8XGvl2OTMJ_Sh9G9vhE-HjAcovcHfumcdoQh_WM445Za6Pyn9BZQV3FCqMviRR809sIATfU5lu86wu_5UGIGI7MFDEYeVGSqzpzh6mlcn8QSIZoYXV0aERhdGFYxEmWDeWIDoxodDQXD2R2YFuP5K65ooYyx5lc87qDHZdjQQAAAAAAAAAAAAAAAAAAAAAAAAAAAEAsV2gIUlPIHzZnNIlQdz5zvbKtpFz_WY-8ZfxOgTyy7f3Ffbolyp3fUtSQo5LfoUgBaBaXqK0wqqYO-u6FrrLApQECAyYgASFYIPr9-YH8DuBsOnaI3KJa0a39hyxh9LDtHErNvfQSyxQsIlgg4rAuQQ5uy4VXGFbkiAt0uwgJJodp-DymkoBcrGsLtkI"},"type":"public-key"}'; + $request = $request->withParsedBody( + json_decode($data, true) + ); + $this->Trait->setRequest($request); + $this->expectException(\Exception::class); + $this->expectErrorMessage('Testing error exception for webauthn2faRegister'); + $this->Trait->webauthn2faRegister(); + } + + /** + * Test webauthn2faAuthenticateOptions + * + * @return void + */ + public function testWebauthn2faAuthenticateOptions() + { + $request = $this->getMockBuilder('Cake\Http\ServerRequest') + ->setMethods(['getSession', 'is']) + ->getMock(); + $this->Trait->setRequest($request); + $request->expects($this->any()) + ->method('is') + ->with( + $this->equalTo('ssl') + )->will($this->returnValue(true)); + + $table = TableRegistry::getTableLocator() + ->get('CakeDC/Users.Users'); + $user = $table->get('00000000-0000-0000-0000-000000000001'); + $this->_mockSession([ + 'Webauthn2fa.User' => $user, + ]); + + $response = $this->Trait->webauthn2faAuthenticateOptions(); + $data = json_decode((string)$response->getBody(), true); + $this->assertArrayHasKey('challenge', $data); + $this->assertArrayHasKey('userVerification', $data); + $this->assertArrayHasKey('allowCredentials', $data); + $expectedCredentials = [ + [ + 'type' => 'public-key', + 'id' => '12b37486-9299-4331-ac33-85b2d985b6fe', + ], + ]; + $this->assertEquals($expectedCredentials, $data['allowCredentials']); + $this->assertSame( + $user, + $this->Trait->getRequest()->getSession()->read('Webauthn2fa.User') + ); + } + + /** + * Test webauthn2faAuthenticateOptions method + * + * @return void + */ + public function testWebauthn2faAuthenticate() + { + $table = TableRegistry::getTableLocator() + ->get('CakeDC/Users.Users'); + $user = $table->get('00000000-0000-0000-0000-000000000002'); + $request = ServerRequestFactory::fromGlobals(); + $request->getSession()->write('Webauthn2fa.User', $user); + + $this->Trait->setRequest($request); + $adapter = $this->getMockBuilder(AuthenticateAdapter::class) + ->onlyMethods(['verifyResponse']) + ->setConstructorArgs([$request]) + ->getMock(); + $publicKeyCredentialId = '12b37486-9299-4331-ac33-85b2d985b6fe'; + $userId = '00000000-0000-0000-0000-000000000002'; + $credentialData = [ + 'publicKeyCredentialId' => $publicKeyCredentialId, + 'type' => 'public-key', + 'transports' => [], + 'attestationType' => 'none', + 'trustPath' => [ + 'type' => 'Webauthn\TrustPath\EmptyTrustPath', + ], + 'aaguid' => '00000000-0000-0000-0000-000000000000', + 'credentialPublicKey' => Base64Url::encode('000000000000000000000000000000000000-9999999999999999999999999999999999999999-XXXXXXXXXXXXX-YYYYYYYYYYY'), + 'userHandle' => Base64Url::encode($userId), + 'counter' => 191, + 'otherUI' => null, + ]; + $credential = PublicKeyCredentialSource::createFromArray($credentialData); + $adapter->expects($this->once()) + ->method('verifyResponse') + ->willReturn($credential); + + $traitMockMethods = array_unique(array_merge(['getUsersTable', 'getWebauthn2faAuthenticateAdapter'], $this->traitMockMethods)); + $this->Trait = $this->getMockBuilder($this->traitClassName) + ->setMethods($traitMockMethods) + ->getMock(); + $this->Trait->expects($this->once()) + ->method('getWebauthn2faAuthenticateAdapter') + ->willReturn($adapter); + $this->Trait->expects($this->any()) + ->method('getUsersTable') + ->will($this->returnValue($this->table)); + $this->Trait->setRequest($request); + $response = $this->Trait->webauthn2faAuthenticate(); + $expected = [ + 'success' => true, + 'redirectUrl' => '/login', + ]; + $actual = json_decode((string)$response->getBody(), true); + $this->assertEquals($expected, $actual); + + $this->assertNull( + $this->Trait->getRequest()->getSession()->read('Webauthn2fa.User') + ); + $userSession = $this->Trait->getRequest()->getSession()->read('TwoFactorAuthenticator.User'); + $this->assertInstanceOf( + User::class, + $userSession + ); + $this->assertEquals($userSession->toArray(), $user->toArray()); + } + + /** + * Test webauthn2faAuthenticateOptions method + * + * @return void + */ + public function testWebauthn2faAuthenticateError() + { + $table = TableRegistry::getTableLocator() + ->get('CakeDC/Users.Users'); + $user = $table->get('00000000-0000-0000-0000-000000000002'); + $request = ServerRequestFactory::fromGlobals(); + $request->getSession()->write('Webauthn2fa.User', $user); + + $this->Trait->setRequest($request); + $adapter = $this->getMockBuilder(AuthenticateAdapter::class) + ->onlyMethods(['verifyResponse']) + ->setConstructorArgs([$request]) + ->getMock(); + + $adapter->expects($this->once()) + ->method('verifyResponse') + ->willThrowException(new \Exception('Test exception error for webauthn2faAuthenticate')); + + $traitMockMethods = array_unique(array_merge(['getUsersTable', 'getWebauthn2faAuthenticateAdapter'], $this->traitMockMethods)); + $this->Trait = $this->getMockBuilder($this->traitClassName) + ->setMethods($traitMockMethods) + ->getMock(); + $this->Trait->expects($this->once()) + ->method('getWebauthn2faAuthenticateAdapter') + ->willReturn($adapter); + $this->Trait->expects($this->any()) + ->method('getUsersTable') + ->will($this->returnValue($this->table)); + $this->Trait->setRequest($request); + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Test exception error for webauthn2faAuthenticate'); + $this->Trait->webauthn2faAuthenticate(); + } +} diff --git a/tests/TestCase/Controller/UsersControllerTest.php b/tests/TestCase/Controller/UsersControllerTest.php index a1f2505a5..9cebd1f89 100644 --- a/tests/TestCase/Controller/UsersControllerTest.php +++ b/tests/TestCase/Controller/UsersControllerTest.php @@ -51,7 +51,7 @@ public function testUnauthorizedRedirectCustomCallable() ], ]); $this->get('/users/index'); - $this->assertRedirect('/my/custom/url/'); + $this->assertRedirectContains('/my/custom/url/'); } /** @@ -67,7 +67,7 @@ public function testUnauthorizedRedirectNotLogged() ], ]); $this->get('/users/index'); - $this->assertRedirect('/login?redirect=http%3A%2F%2Flocalhost%2Fusers%2Findex'); + $this->assertRedirectContains('/login?redirect=http%3A%2F%2Flocalhost%2Fusers%2Findex'); } /** @@ -85,10 +85,10 @@ public function testUnauthorizedRedirectLogged() $this->session(['Auth' => $user]); $this->configRequest([ 'headers' => [ - 'REFERER' => 'http://localhost/profile', + 'REFERER' => 'http://example.com/profile', ], ]); $this->get('/users/index'); - $this->assertRedirect('/profile'); + $this->assertRedirectContains('/profile'); } } diff --git a/tests/TestCase/Mailer/UsersMailerTest.php b/tests/TestCase/Mailer/UsersMailerTest.php index 8c05b4ea1..294c245e9 100644 --- a/tests/TestCase/Mailer/UsersMailerTest.php +++ b/tests/TestCase/Mailer/UsersMailerTest.php @@ -170,7 +170,6 @@ public function testResetPassword() * @param object &$object Instantiated object that we will run method on. * @param string $methodName Method name to call * @param array $parameters Array of parameters to pass into method. - * * @return mixed Method return. */ public function invokeMethod(&$object, $methodName, $parameters = []) diff --git a/tests/TestCase/Middleware/SocialAuthMiddlewareTest.php b/tests/TestCase/Middleware/SocialAuthMiddlewareTest.php index de714d186..6bbb8e28d 100644 --- a/tests/TestCase/Middleware/SocialAuthMiddlewareTest.php +++ b/tests/TestCase/Middleware/SocialAuthMiddlewareTest.php @@ -213,7 +213,7 @@ public function testSuccessfullyAuthenticated() public function dataProviderSocialAuthenticationException() { $missingEmail = [ - new MissingEmailException("Missing email"), + new MissingEmailException('Missing email'), [ 'key' => 'flash', 'element' => 'Flash/error', @@ -224,7 +224,7 @@ public function dataProviderSocialAuthenticationException() true, ]; $unknown = [ - new UnexpectedValueException("User not active"), + new UnexpectedValueException('User not active'), [ 'key' => 'flash', 'element' => 'Flash/error', @@ -253,14 +253,15 @@ public function dataProviderSocialAuthenticationException() */ public function testSocialAuthenticationException($previousException, $flash, $location, $keepSocialUser) { - Router::connect('/login', [ + $builder = Router::createRouteBuilder('/'); + $builder->connect('/login', [ 'plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'login', '_ext' => null, 'prefix' => null, ]); - Router::connect('/users/users/social-email', [ + $builder->connect('/users/users/social-email', [ 'plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'socialEmail', diff --git a/tests/TestCase/Model/Behavior/AuthFinderBehaviorTest.php b/tests/TestCase/Model/Behavior/AuthFinderBehaviorTest.php index ea5b2fb2b..b98fa6dac 100644 --- a/tests/TestCase/Model/Behavior/AuthFinderBehaviorTest.php +++ b/tests/TestCase/Model/Behavior/AuthFinderBehaviorTest.php @@ -57,7 +57,6 @@ public function tearDown(): void /** * Test findActive method. - * */ public function testFindActive() { diff --git a/tests/TestCase/Model/Behavior/LinkSocialBehaviorTest.php b/tests/TestCase/Model/Behavior/LinkSocialBehaviorTest.php index 0a4eba8ac..df6e5620e 100644 --- a/tests/TestCase/Model/Behavior/LinkSocialBehaviorTest.php +++ b/tests/TestCase/Model/Behavior/LinkSocialBehaviorTest.php @@ -13,10 +13,10 @@ namespace CakeDC\Users\Test\TestCase\Model\Behavior; -use Cake\I18n\Time; use Cake\ORM\TableRegistry; use Cake\TestSuite\TestCase; use CakeDC\Users\Model\Behavior\LinkSocialBehavior; +use DateTime; /** * App\Model\Behavior\LinkSocialBehavior Test Case @@ -69,7 +69,6 @@ public function tearDown(): void * @param array $data Test input data * @param string $userId User id to add social account * @param array $result Expected result - * * @author Marcelo Rocha * @return void * @dataProvider providerFacebookLinkSocialAccount @@ -102,7 +101,7 @@ public function testlinkSocialAccountFacebookProvider($data, $userId, $result) */ public function providerFacebookLinkSocialAccount() { - $expiresTime = new Time(); + $expiresTime = new DateTime(); $tokenExpires = $expiresTime->setTimestamp(1458423682)->format('Y-m-d H:i:s'); return [ @@ -160,7 +159,6 @@ public function providerFacebookLinkSocialAccount() * * @param array $data Test input data * @param string $userId User id to add social account - * * @author Marcelo Rocha * @return void * @dataProvider providerFacebookLinkSocialAccountErrorSaving @@ -197,7 +195,7 @@ public function testlinkSocialAccountErrorSavingFacebookProvider($data, $userId) */ public function providerFacebookLinkSocialAccountErrorSaving() { - $expiresTime = new Time(); + $expiresTime = new DateTime(); $tokenExpires = $expiresTime->setTimestamp(1458423682)->format('Y-m-d H:i:s'); return [ @@ -256,7 +254,6 @@ public function providerFacebookLinkSocialAccountErrorSaving() * @param array $data Test input data * @param string $userId User id to add social account * @param array $result Expected result - * * @author Marcelo Rocha * @return void * @dataProvider providerFacebookLinkSocialAccountAccountExists @@ -269,7 +266,7 @@ public function testlinkSocialAccountFacebookProviderAccountExists($data, $userI $this->assertFalse($resultUser->has('social_accounts')); $expected = [ 'social_accounts' => [ - '_existsIn' => __d('cake_d_c/Users', 'Social account already associated to another user'), + '_existsIn' => __d('cake_d_c/users', 'Social account already associated to another user'), ], ]; $actual = $user->getErrors(); @@ -306,7 +303,7 @@ public function testlinkSocialAccountFacebookProviderAccountExists($data, $userI */ public function providerFacebookLinkSocialAccountAccountExists() { - $expiresTime = new Time(); + $expiresTime = new DateTime(); $tokenExpires = $expiresTime->setTimestamp(1458423682)->format('Y-m-d H:i:s'); return [ diff --git a/tests/TestCase/Model/Behavior/PasswordBehaviorTest.php b/tests/TestCase/Model/Behavior/PasswordBehaviorTest.php index 97268841b..23b12f02f 100644 --- a/tests/TestCase/Model/Behavior/PasswordBehaviorTest.php +++ b/tests/TestCase/Model/Behavior/PasswordBehaviorTest.php @@ -28,6 +28,7 @@ /** * Test Case + * * @property \CakeDC\Users\Model\Behavior\PasswordBehavior Behavior */ class PasswordBehaviorTest extends TestCase @@ -79,7 +80,6 @@ public function tearDown(): void /** * Test resetToken - * */ public function testResetToken() { @@ -99,7 +99,6 @@ public function testResetToken() /** * Test resetToken - * */ public function testResetTokenSendEmail() { diff --git a/tests/TestCase/Model/Behavior/RegisterBehaviorTest.php b/tests/TestCase/Model/Behavior/RegisterBehaviorTest.php index bdb843b23..317cc6a80 100644 --- a/tests/TestCase/Model/Behavior/RegisterBehaviorTest.php +++ b/tests/TestCase/Model/Behavior/RegisterBehaviorTest.php @@ -92,7 +92,7 @@ public function testValidateRegisterNoValidateEmail() 'last_name' => 'user', 'tos' => 1, ]; - $result = $this->Table->register($this->Table->newEntity([]), $user, ['token_expiration' => 3600, 'validate_email' => 0]); + $result = $this->Table->register($this->Table->newEmptyEntity(), $user, ['token_expiration' => 3600, 'validate_email' => 0]); $this->assertTrue($result->active); } @@ -104,7 +104,7 @@ public function testValidateRegisterNoValidateEmail() public function testValidateRegisterEmptyUser() { $user = []; - $result = $this->Table->register($this->Table->newEntity([]), $user, ['token_expiration' => 3600, 'validate_email' => 1]); + $result = $this->Table->register($this->Table->newEmptyEntity(), $user, ['token_expiration' => 3600, 'validate_email' => 1]); $this->assertFalse($result); } @@ -115,7 +115,8 @@ public function testValidateRegisterEmptyUser() */ public function testValidateRegisterValidateEmailAndTos() { - Router::connect('/users/validate-email/*', [ + $builder = Router::createRouteBuilder('/'); + $builder->connect('/users/validate-email/*', [ 'plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'validateEmail', @@ -130,7 +131,7 @@ public function testValidateRegisterValidateEmailAndTos() 'last_name' => 'user', 'tos' => 1, ]; - $result = $this->Table->register($this->Table->newEntity([]), $user, ['token_expiration' => 3600, 'validate_email' => 1]); + $result = $this->Table->register($this->Table->newEmptyEntity(), $user, ['token_expiration' => 3600, 'validate_email' => 1]); $this->assertNotEmpty($result); $this->assertFalse($result->active); $this->assertNotEmpty($result->tos_date); @@ -143,7 +144,8 @@ public function testValidateRegisterValidateEmailAndTos() */ public function testValidateRegisterValidatorOption() { - Router::connect('/users/validate-email/*', [ + $builder = Router::createRouteBuilder('/'); + $builder->connect('/users/validate-email/*', [ 'plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'validateEmail', @@ -177,7 +179,7 @@ public function testValidateRegisterValidatorOption() $this->Table->expects($this->once()) ->method('patchEntity') - ->with($this->Table->newEntity([]), $user, ['validate' => 'custom']) + ->with($this->Table->newEmptyEntity(), $user, ['validate' => 'custom']) ->will($this->returnValue($entityUser)); $this->Table->expects($this->once()) @@ -185,13 +187,12 @@ public function testValidateRegisterValidatorOption() ->with($entityUser) ->will($this->returnValue($entityUser)); - $result = $this->Behavior->register($this->Table->newEntity([]), $user, ['validator' => 'custom', 'validate_email' => 1]); + $result = $this->Behavior->register($this->Table->newEmptyEntity(), $user, ['validator' => 'custom', 'validate_email' => 1]); $this->assertNotEmpty($result->tos_date); } /** * Test register method - * */ public function testValidateRegisterTosRequired() { @@ -203,7 +204,7 @@ public function testValidateRegisterTosRequired() 'first_name' => 'test', 'last_name' => 'user', ]; - $result = $this->Table->register($this->Table->newEntity([]), $user, ['token_expiration' => 3600, 'validate_email' => 1, 'use_tos' => 1]); + $result = $this->Table->register($this->Table->newEmptyEntity(), $user, ['token_expiration' => 3600, 'validate_email' => 1, 'use_tos' => 1]); $this->assertFalse($result); } @@ -214,7 +215,8 @@ public function testValidateRegisterTosRequired() */ public function testValidateRegisterNoTosRequired() { - Router::connect('/users/validate-email/*', [ + $builder = Router::createRouteBuilder('/'); + $builder->connect('/users/validate-email/*', [ 'plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'validateEmail', @@ -228,7 +230,7 @@ public function testValidateRegisterNoTosRequired() 'first_name' => 'test', 'last_name' => 'user', ]; - $result = $this->Table->register($this->Table->newEntity([]), $user, ['token_expiration' => 3600, 'validate_email' => 1, 'use_tos' => 0]); + $result = $this->Table->register($this->Table->newEmptyEntity(), $user, ['token_expiration' => 3600, 'validate_email' => 1, 'use_tos' => 0]); $this->assertNotEmpty($result); } @@ -313,7 +315,7 @@ public function testRegisterUsingDefaultRole() 'tos' => 1, ]; Configure::write('Users.Registration.defaultRole', false); - $result = $this->Table->register($this->Table->newEntity([]), $user, [ + $result = $this->Table->register($this->Table->newEmptyEntity(), $user, [ 'token_expiration' => 3600, 'validate_email' => 0, ]); @@ -337,7 +339,7 @@ public function testRegisterUsingCustomRole() 'tos' => 1, ]; Configure::write('Users.Registration.defaultRole', 'emperor'); - $result = $this->Table->register($this->Table->newEntity([]), $user, [ + $result = $this->Table->register($this->Table->newEmptyEntity(), $user, [ 'token_expiration' => 3600, 'validate_email' => 0, ]); diff --git a/tests/TestCase/Model/Behavior/SocialBehaviorTest.php b/tests/TestCase/Model/Behavior/SocialBehaviorTest.php index decc19871..684f6c4db 100644 --- a/tests/TestCase/Model/Behavior/SocialBehaviorTest.php +++ b/tests/TestCase/Model/Behavior/SocialBehaviorTest.php @@ -126,7 +126,6 @@ public function testSocialLoginFacebookProviderUsingEmail($data, $options, $data /** * Provider for socialLogin with facebook and not existing user - * */ public function providerFacebookSocialLogin() { @@ -255,7 +254,6 @@ public function testSocialLoginExistingReferenceOkay($data, $options) /** * Provider for socialLogin with facebook with existing and active user - * */ public function providerFacebookSocialLoginExistingReference() { @@ -296,7 +294,6 @@ public function testSocialLoginExistingNotActiveReference($data, $options) /** * Provider for socialLogin with existing and active user and not active social account - * */ public function providerSocialLoginExistingAndNotActiveAccount() { @@ -337,7 +334,6 @@ public function testSocialLoginExistingReferenceNotActiveUser($data, $options) /** * Provider for socialLogin with existing and active account but not active user - * */ public function providerSocialLoginExistingAccountNotActiveUser() { @@ -370,7 +366,6 @@ public function testSocialLoginNoEmail($data, $options) /** * Provider for socialLogin with facebook and not existing user - * */ public function providerFacebookSocialLoginNoEmail() { @@ -414,7 +409,6 @@ public function testGenerateUniqueUsername($param, $expected) /** * Provider for socialLogin with facebook and not existing user - * */ public function providerGenerateUsername() { diff --git a/tests/TestCase/Model/Entity/UserTest.php b/tests/TestCase/Model/Entity/UserTest.php index 401843eb7..4f0981f71 100644 --- a/tests/TestCase/Model/Entity/UserTest.php +++ b/tests/TestCase/Model/Entity/UserTest.php @@ -14,8 +14,8 @@ namespace CakeDC\Users\Test\TestCase\Model\Entity; use Cake\Auth\DefaultPasswordHasher; +use Cake\I18n\FrozenTime; use Cake\I18n\I18n; -use Cake\I18n\Time; use Cake\TestSuite\TestCase; use CakeDC\Users\Model\Entity\User; @@ -32,8 +32,8 @@ class UserTest extends TestCase public function setUp(): void { parent::setUp(); - $this->now = Time::now(); - Time::setTestNow($this->now); + $this->now = FrozenTime::now(); + FrozenTime::setTestNow($this->now); $this->User = new User(); } @@ -45,7 +45,7 @@ public function setUp(): void public function tearDown(): void { unset($this->User); - Time::setTestNow(); + FrozenTime::setTestNow(); parent::tearDown(); } diff --git a/tests/TestCase/Model/Table/UsersTableTest.php b/tests/TestCase/Model/Table/UsersTableTest.php index b3ef36c05..58b1e7fd6 100644 --- a/tests/TestCase/Model/Table/UsersTableTest.php +++ b/tests/TestCase/Model/Table/UsersTableTest.php @@ -85,7 +85,7 @@ public function testValidateRegisterNoValidateEmail() 'last_name' => 'user', 'tos' => 1, ]; - $result = $this->Users->register($this->Users->newEntity([]), $user, ['token_expiration' => 3600, 'validate_email' => 0]); + $result = $this->Users->register($this->Users->newEmptyEntity(), $user, ['token_expiration' => 3600, 'validate_email' => 0]); $this->assertTrue($result->active); } @@ -97,7 +97,7 @@ public function testValidateRegisterNoValidateEmail() public function testValidateRegisterEmptyUser() { $user = []; - $result = $this->Users->register($this->Users->newEntity([]), $user, ['token_expiration' => 3600, 'validate_email' => 1]); + $result = $this->Users->register($this->Users->newEmptyEntity(), $user, ['token_expiration' => 3600, 'validate_email' => 1]); $this->assertFalse($result); } @@ -108,7 +108,8 @@ public function testValidateRegisterEmptyUser() */ public function testValidateRegisterValidateEmail() { - Router::connect('/users/validate-email/*', [ + $builder = Router::createRouteBuilder('/'); + $builder->connect('/users/validate-email/*', [ 'plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'validateEmail', @@ -123,7 +124,7 @@ public function testValidateRegisterValidateEmail() 'last_name' => 'user', 'tos' => 1, ]; - $result = $this->Users->register($this->Users->newEntity([]), $user, ['token_expiration' => 3600, 'validate_email' => 1]); + $result = $this->Users->register($this->Users->newEmptyEntity(), $user, ['token_expiration' => 3600, 'validate_email' => 1]); $this->assertNotEmpty($result); $this->assertFalse($result->active); } @@ -141,7 +142,7 @@ public function testValidateRegisterTosRequired() 'first_name' => 'test', 'last_name' => 'user', ]; - $userEntity = $this->Users->newEntity([]); + $userEntity = $this->Users->newEmptyEntity(); $this->Users->register($userEntity, $user, ['token_expiration' => 3600, 'validate_email' => 1, 'use_tos' => 1]); $this->assertEquals(['tos' => ['_required' => 'This field is required']], $userEntity->getErrors()); } @@ -152,7 +153,8 @@ public function testValidateRegisterTosRequired() */ public function testValidateRegisterNoTosRequired() { - Router::connect('/users/validate-email/*', [ + $builder = Router::createRouteBuilder('/'); + $builder->connect('/users/validate-email/*', [ 'plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'validateEmail', @@ -166,7 +168,7 @@ public function testValidateRegisterNoTosRequired() 'first_name' => 'test', 'last_name' => 'user', ]; - $result = $this->Users->register($this->Users->newEntity([]), $user, ['token_expiration' => 3600, 'validate_email' => 1, 'use_tos' => 0]); + $result = $this->Users->register($this->Users->newEmptyEntity(), $user, ['token_expiration' => 3600, 'validate_email' => 1, 'use_tos' => 0]); $this->assertNotEmpty($result); } @@ -270,7 +272,6 @@ public function testSocialLoginCreateNewAccountWithNoCredentials() /** * Test socialLogin - * */ public function testSocialLoginCreateNewAccount() { diff --git a/tests/TestCase/PluginTest.php b/tests/TestCase/PluginTest.php index befaaa762..d4fedf44f 100644 --- a/tests/TestCase/PluginTest.php +++ b/tests/TestCase/PluginTest.php @@ -13,26 +13,13 @@ namespace CakeDC\Users\Test\TestCase; -use Authentication\Authenticator\SessionAuthenticator; -use Authentication\Authenticator\TokenAuthenticator; -use Authentication\Identifier\JwtSubjectIdentifier; -use Authentication\Identifier\PasswordIdentifier; -use Authentication\Identifier\TokenIdentifier; use Authentication\Middleware\AuthenticationMiddleware; -use Authorization\AuthorizationService; use Authorization\Middleware\AuthorizationMiddleware; use Authorization\Middleware\RequestAuthorizationMiddleware; -use Authorization\Policy\ResolverCollection; use Cake\Core\Configure; use Cake\Http\MiddlewareQueue; -use Cake\Http\Response; -use Cake\Http\ServerRequest; -use Cake\Http\ServerRequestFactory; use Cake\TestSuite\IntegrationTestTrait; use Cake\TestSuite\TestCase; -use CakeDC\Auth\Authentication\AuthenticationService as CakeDCAuthenticationService; -use CakeDC\Auth\Authenticator\FormAuthenticator; -use CakeDC\Auth\Authenticator\TwoFactorAuthenticator; use CakeDC\Auth\Middleware\TwoFactorMiddleware; use CakeDC\Users\Middleware\SocialAuthMiddleware; use CakeDC\Users\Middleware\SocialEmailMiddleware; @@ -211,251 +198,6 @@ public function testMiddlewareNotGoogleAuthenticationAndNotSocial() $this->assertInstanceOf(RequestAuthorizationMiddleware::class, $middleware->current()); } - /** - * testGetAuthenticationService - * - * @return void - */ - public function testGetAuthenticationServiceCallableDefined() - { - $request = ServerRequestFactory::fromGlobals(); - $request->withQueryParams(['method' => __METHOD__]); - $service = new CakeDCAuthenticationService([ - 'identifiers' => [ - 'Authentication.Password', - ], - ]); - Configure::write('Auth.Authentication.serviceLoader', function ($aRequest) use ($request, $service) { - $this->assertSame($request, $aRequest); - - return $service; - }); - - $plugin = new Plugin(); - $actualService = $plugin->getAuthenticationService($request); - $this->assertSame($service, $actualService); - } - - /** - * testGetAuthenticationService - * - * @return void - */ - public function testGetAuthenticationService() - { - Configure::write('Auth.Authenticators', [ - 'Session' => [ - 'className' => 'Authentication.Session', - 'skipTwoFactorVerify' => true, - 'sessionKey' => 'CustomAuth', - 'fields' => ['username' => 'email'], - 'identify' => true, - ], - 'Form' => [ - 'className' => 'CakeDC/Auth.Form', - 'loginUrl' => '/login', - 'fields' => ['username' => 'email', 'password' => 'alt_password'], - ], - 'Token' => [ - 'className' => 'Authentication.Token', - 'skipTwoFactorVerify' => true, - 'header' => null, - 'queryParam' => 'api_key', - 'tokenPrefix' => null, - ], - ]); - Configure::write('Auth.Identifiers', [ - 'Password' => [ - 'className' => 'Authentication.Password', - 'fields' => [ - 'username' => 'email_2', - 'password' => 'password_2', - ], - ], - 'Token' => [ - 'className' => 'Authentication.Token', - 'tokenField' => 'api_token', - ], - 'Authentication.JwtSubject', - ]); - Configure::write('OneTimePasswordAuthenticator.login', true); - - $plugin = new Plugin(); - $service = $plugin->getAuthenticationService(new ServerRequest(), new Response()); - $this->assertInstanceOf(CakeDCAuthenticationService::class, $service); - - /** - * @var \Authentication\Authenticator\AuthenticatorCollection $authenticators - */ - $authenticators = $service->authenticators(); - $expected = [ - SessionAuthenticator::class => [ - 'fields' => ['username' => 'email'], - 'sessionKey' => 'CustomAuth', - 'identify' => true, - 'identityAttribute' => 'identity', - 'skipTwoFactorVerify' => true, - ], - FormAuthenticator::class => [ - 'loginUrl' => '/login', - 'keyCheckEnabledRecaptcha' => 'Users.reCaptcha.login', - 'fields' => ['username' => 'email', 'password' => 'alt_password'], - ], - TokenAuthenticator::class => [ - 'header' => null, - 'queryParam' => 'api_key', - 'tokenPrefix' => null, - 'skipTwoFactorVerify' => true, - ], - TwoFactorAuthenticator::class => [ - 'loginUrl' => null, - 'urlChecker' => 'Authentication.Default', - 'skipTwoFactorVerify' => true, - ], - ]; - $actual = []; - foreach ($authenticators as $key => $value) { - $actual[get_class($value)] = $value->getConfig(); - } - $this->assertEquals($expected, $actual); - - /** - * @var \Authentication\Identifier\IdentifierCollection $identifiers - */ - $identifiers = $service->identifiers(); - $expected = [ - PasswordIdentifier::class => [ - 'fields' => [ - 'username' => 'email_2', - 'password' => 'password_2', - ], - 'resolver' => 'Authentication.Orm', - 'passwordHasher' => null, - ], - TokenIdentifier::class => [ - 'tokenField' => 'api_token', - 'dataField' => 'token', - 'resolver' => 'Authentication.Orm', - ], - JwtSubjectIdentifier::class => [ - 'tokenField' => 'id', - 'dataField' => 'sub', - 'resolver' => 'Authentication.Orm', - ], - ]; - $actual = []; - foreach ($identifiers as $key => $value) { - $actual[get_class($value)] = $value->getConfig(); - } - $this->assertEquals($expected, $actual); - } - - /** - * testGetAuthenticationService - * - * @return void - */ - public function testGetAuthenticationServiceWithouOneTimePasswordAuthenticator() - { - Configure::write('Auth.Authenticators', [ - 'Session' => [ - 'className' => 'Authentication.Session', - 'skipTwoFactorVerify' => true, - 'sessionKey' => 'CustomAuth', - 'fields' => ['username' => 'email'], - 'identify' => true, - ], - 'Form' => [ - 'className' => 'CakeDC/Auth.Form', - 'loginUrl' => '/login', - 'fields' => ['username' => 'email', 'password' => 'alt_password'], - ], - 'Token' => [ - 'className' => 'Authentication.Token', - 'skipTwoFactorVerify' => true, - 'header' => null, - 'queryParam' => 'api_key', - 'tokenPrefix' => null, - ], - ]); - Configure::write('Auth.Identifiers', [ - 'Authentication.Password', - 'Token' => [ - 'className' => 'Authentication.Token', - 'tokenField' => 'api_token', - ], - 'Authentication.JwtSubject', - ]); - Configure::write('OneTimePasswordAuthenticator.login', false); - - $plugin = new Plugin(); - $service = $plugin->getAuthenticationService(new ServerRequest(), new Response()); - $this->assertInstanceOf(CakeDCAuthenticationService::class, $service); - - /** - * @var \Authentication\Authenticator\AuthenticatorCollection $authenticators - */ - $authenticators = $service->authenticators(); - $expected = [ - SessionAuthenticator::class => [ - 'fields' => ['username' => 'email'], - 'sessionKey' => 'CustomAuth', - 'identify' => true, - 'identityAttribute' => 'identity', - 'skipTwoFactorVerify' => true, - ], - FormAuthenticator::class => [ - 'loginUrl' => '/login', - 'fields' => ['username' => 'email', 'password' => 'alt_password'], - 'keyCheckEnabledRecaptcha' => 'Users.reCaptcha.login', - ], - TokenAuthenticator::class => [ - 'header' => null, - 'queryParam' => 'api_key', - 'tokenPrefix' => null, - 'skipTwoFactorVerify' => true, - ], - ]; - $actual = []; - foreach ($authenticators as $key => $value) { - $actual[get_class($value)] = $value->getConfig(); - } - $this->assertEquals($expected, $actual); - } - - /** - * testGetAuthorizationService - * - * @return void - */ - public function testGetAuthorizationService() - { - $plugin = new Plugin(); - $service = $plugin->getAuthorizationService(new ServerRequest()); - $this->assertInstanceOf(AuthorizationService::class, $service); - } - - /** - * testGetAuthorizationService - * - * @return void - */ - public function testGetAuthorizationServiceCallableDefined() - { - $request = ServerRequestFactory::fromGlobals(); - $request->withQueryParams(['method' => __METHOD__]); - $service = new AuthorizationService(new ResolverCollection()); - Configure::write('Auth.Authorization.serviceLoader', function ($aRequest) use ($request, $service) { - $this->assertSame($request, $aRequest); - - return $service; - }); - - $plugin = new Plugin(); - $actualService = $plugin->getAuthorizationService($request); - $this->assertSame($service, $actualService); - } - /** * test bootstrap method * diff --git a/tests/TestCase/Provider/AuthenticationServiceProviderTest.php b/tests/TestCase/Provider/AuthenticationServiceProviderTest.php new file mode 100644 index 000000000..710402173 --- /dev/null +++ b/tests/TestCase/Provider/AuthenticationServiceProviderTest.php @@ -0,0 +1,249 @@ + [ + 'className' => 'Authentication.Session', + 'skipTwoFactorVerify' => true, + 'sessionKey' => 'CustomAuth', + 'fields' => ['username' => 'email'], + 'identify' => true, + ], + 'Form' => [ + 'className' => 'CakeDC/Auth.Form', + 'loginUrl' => '/login', + 'fields' => ['username' => 'email', 'password' => 'alt_password'], + ], + 'Token' => [ + 'className' => 'Authentication.Token', + 'skipTwoFactorVerify' => true, + 'header' => null, + 'queryParam' => 'api_key', + 'tokenPrefix' => null, + ], + ]); + Configure::write('Auth.Identifiers', [ + 'Password' => [ + 'className' => 'Authentication.Password', + 'fields' => [ + 'username' => 'email_2', + 'password' => 'password_2', + ], + ], + 'Token' => [ + 'className' => 'Authentication.Token', + 'tokenField' => 'api_token', + ], + 'Authentication.JwtSubject', + ]); + Configure::write('OneTimePasswordAuthenticator.login', true); + + $authenticationServiceProvider = new AuthenticationServiceProvider(); + $service = $authenticationServiceProvider->getAuthenticationService(new ServerRequest(), new Response()); + $this->assertInstanceOf(CakeDCAuthenticationService::class, $service); + + /** + * @var \Authentication\Authenticator\AuthenticatorCollection $authenticators + */ + $authenticators = $service->authenticators(); + $expected = [ + SessionAuthenticator::class => [ + 'fields' => ['username' => 'email'], + 'sessionKey' => 'CustomAuth', + 'identify' => true, + 'identityAttribute' => 'identity', + 'skipTwoFactorVerify' => true, + ], + FormAuthenticator::class => [ + 'loginUrl' => '/login', + 'keyCheckEnabledRecaptcha' => 'Users.reCaptcha.login', + 'fields' => ['username' => 'email', 'password' => 'alt_password'], + ], + TokenAuthenticator::class => [ + 'header' => null, + 'queryParam' => 'api_key', + 'tokenPrefix' => null, + 'skipTwoFactorVerify' => true, + ], + TwoFactorAuthenticator::class => [ + 'loginUrl' => null, + 'urlChecker' => 'Authentication.Default', + 'skipTwoFactorVerify' => true, + ], + ]; + $actual = []; + foreach ($authenticators as $key => $value) { + $actual[get_class($value)] = $value->getConfig(); + } + $this->assertEquals($expected, $actual); + + /** + * @var \Authentication\Identifier\IdentifierCollection $identifiers + */ + $identifiers = $service->identifiers(); + $expected = [ + PasswordIdentifier::class => [ + 'fields' => [ + 'username' => 'email_2', + 'password' => 'password_2', + ], + 'resolver' => 'Authentication.Orm', + 'passwordHasher' => null, + ], + TokenIdentifier::class => [ + 'tokenField' => 'api_token', + 'dataField' => 'token', + 'resolver' => 'Authentication.Orm', + ], + JwtSubjectIdentifier::class => [ + 'tokenField' => 'id', + 'dataField' => 'sub', + 'resolver' => 'Authentication.Orm', + ], + ]; + $actual = []; + foreach ($identifiers as $key => $value) { + $actual[get_class($value)] = $value->getConfig(); + } + $this->assertEquals($expected, $actual); + } + + /** + * testGetAuthenticationService + * + * @return void + */ + public function testGetAuthenticationServiceCallableDefined() + { + $request = ServerRequestFactory::fromGlobals(); + $request->withQueryParams(['method' => __METHOD__]); + $service = new CakeDCAuthenticationService([ + 'identifiers' => [ + 'Authentication.Password', + ], + ]); + Configure::write('Auth.Authentication.serviceLoader', function ($aRequest) use ($request, $service) { + $this->assertSame($request, $aRequest); + + return $service; + }); + + $authenticationServiceProvider = new AuthenticationServiceProvider(); + $actualService = $authenticationServiceProvider->getAuthenticationService($request); + $this->assertSame($service, $actualService); + } + + /** + * testGetAuthenticationService + * + * @return void + */ + public function testGetAuthenticationServiceWithoutOneTimePasswordAuthenticator() + { + Configure::write('Auth.Authenticators', [ + 'Session' => [ + 'className' => 'Authentication.Session', + 'skipTwoFactorVerify' => true, + 'sessionKey' => 'CustomAuth', + 'fields' => ['username' => 'email'], + 'identify' => true, + ], + 'Form' => [ + 'className' => 'CakeDC/Auth.Form', + 'loginUrl' => '/login', + 'fields' => ['username' => 'email', 'password' => 'alt_password'], + ], + 'Token' => [ + 'className' => 'Authentication.Token', + 'skipTwoFactorVerify' => true, + 'header' => null, + 'queryParam' => 'api_key', + 'tokenPrefix' => null, + ], + ]); + Configure::write('Auth.Identifiers', [ + 'Authentication.Password', + 'Token' => [ + 'className' => 'Authentication.Token', + 'tokenField' => 'api_token', + ], + 'Authentication.JwtSubject', + ]); + Configure::write('OneTimePasswordAuthenticator.login', false); + + $authenticationServiceProvider = new AuthenticationServiceProvider(); + $service = $authenticationServiceProvider->getAuthenticationService(new ServerRequest(), new Response()); + $this->assertInstanceOf(CakeDCAuthenticationService::class, $service); + + /** + * @var \Authentication\Authenticator\AuthenticatorCollection $authenticators + */ + $authenticators = $service->authenticators(); + $expected = [ + SessionAuthenticator::class => [ + 'fields' => ['username' => 'email'], + 'sessionKey' => 'CustomAuth', + 'identify' => true, + 'identityAttribute' => 'identity', + 'skipTwoFactorVerify' => true, + ], + FormAuthenticator::class => [ + 'loginUrl' => '/login', + 'fields' => ['username' => 'email', 'password' => 'alt_password'], + 'keyCheckEnabledRecaptcha' => 'Users.reCaptcha.login', + ], + TokenAuthenticator::class => [ + 'header' => null, + 'queryParam' => 'api_key', + 'tokenPrefix' => null, + 'skipTwoFactorVerify' => true, + ], + ]; + $actual = []; + foreach ($authenticators as $key => $value) { + $actual[get_class($value)] = $value->getConfig(); + } + $this->assertEquals($expected, $actual); + } +} diff --git a/tests/TestCase/Provider/AuthorizationServiceProviderTest.php b/tests/TestCase/Provider/AuthorizationServiceProviderTest.php new file mode 100644 index 000000000..d8ffb30e5 --- /dev/null +++ b/tests/TestCase/Provider/AuthorizationServiceProviderTest.php @@ -0,0 +1,63 @@ +getAuthorizationService(new ServerRequest()); + $this->assertInstanceOf(AuthorizationService::class, $service); + } + + /** + * testGetAuthorizationService + * + * @return void + */ + public function testGetAuthorizationServiceCallableDefined() + { + $request = ServerRequestFactory::fromGlobals(); + $request->withQueryParams(['method' => __METHOD__]); + $service = new AuthorizationService(new ResolverCollection()); + Configure::write('Auth.Authorization.serviceLoader', function ($aRequest) use ($request, $service) { + $this->assertSame($request, $aRequest); + + return $service; + }); + + $authorizationServiceProvider = new AuthorizationServiceProvider(); + $actualService = $authorizationServiceProvider->getAuthorizationService($request); + $this->assertSame($service, $actualService); + } +} diff --git a/tests/TestCase/Shell/UsersShellTest.php b/tests/TestCase/Shell/UsersShellTest.php index f3864c527..2ce3ea7c0 100644 --- a/tests/TestCase/Shell/UsersShellTest.php +++ b/tests/TestCase/Shell/UsersShellTest.php @@ -290,7 +290,7 @@ public function testResetAllPasswordsNoPassingParams() */ public function testResetPassword() { - $user = $this->Users->newEntity([]); + $user = $this->Users->newEmptyEntity(); $user->username = 'user-1'; $user->password = 'password'; diff --git a/tests/TestCase/Utility/UsersUrlTest.php b/tests/TestCase/Utility/UsersUrlTest.php index 2134a55ce..42db22eee 100644 --- a/tests/TestCase/Utility/UsersUrlTest.php +++ b/tests/TestCase/Utility/UsersUrlTest.php @@ -372,7 +372,6 @@ public function dataProviderCheckActionOnRequest() * @param array $params request params * @param string $controller users controller * @param bool $expected result expected - * * @dataProvider dataProviderCheckActionOnRequest * @return void */ diff --git a/tests/TestCase/View/Helper/AuthLinkHelperTest.php b/tests/TestCase/View/Helper/AuthLinkHelperTest.php index cae56eacb..c69f5393c 100644 --- a/tests/TestCase/View/Helper/AuthLinkHelperTest.php +++ b/tests/TestCase/View/Helper/AuthLinkHelperTest.php @@ -86,7 +86,8 @@ public function testLinkFalseWithMock(): void */ public function testLinkAuthorizedHappy(): void { - Router::connect('/profile', [ + $builder = Router::createRouteBuilder('/'); + $builder->connect('/profile', [ 'plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'profile', @@ -152,7 +153,8 @@ public function testPostLinkAuthorizedAllowedTrueLoggedAsAdmin(): void 'action' => 'delete', '00000000-0000-0000-0000-000000000010', ]; - Router::connect('/Users/delete/00000000-0000-0000-0000-000000000010', $url); + $builder = Router::createRouteBuilder('/'); + $builder->connect('/Users/delete/00000000-0000-0000-0000-000000000010', $url); $this->AuthLink->expects($this->once()) ->method('isAuthorized') ->with( diff --git a/tests/TestCase/View/Helper/UserHelperTest.php b/tests/TestCase/View/Helper/UserHelperTest.php index 447606b08..1ce9a8f58 100644 --- a/tests/TestCase/View/Helper/UserHelperTest.php +++ b/tests/TestCase/View/Helper/UserHelperTest.php @@ -101,7 +101,8 @@ public function tearDown(): void */ public function testLogout() { - Router::connect('/logout', [ + $builder = Router::createRouteBuilder('/'); + $builder->connect('/logout', [ 'plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'logout', @@ -119,7 +120,8 @@ public function testLogout() */ public function testLogoutDifferentMessage() { - Router::connect('/logout', [ + $builder = Router::createRouteBuilder('/'); + $builder->connect('/logout', [ 'plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'logout', @@ -137,7 +139,8 @@ public function testLogoutDifferentMessage() */ public function testLogoutWithOptions() { - Router::connect('/logout', [ + $builder = Router::createRouteBuilder('/'); + $builder->connect('/logout', [ 'plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'logout', @@ -155,7 +158,8 @@ public function testLogoutWithOptions() */ public function testWelcome() { - Router::connect('/profile', [ + $builder = Router::createRouteBuilder('/'); + $builder->connect('/profile', [ 'plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'profile', @@ -183,7 +187,8 @@ public function testWelcome() */ public function testWelcomeWillDisplayUsernameInstead() { - Router::connect('/profile', [ + $builder = Router::createRouteBuilder('/'); + $builder->connect('/profile', [ 'plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'profile', @@ -251,7 +256,7 @@ public function testAddReCaptchaEmpty() */ public function testAddReCaptchaScript() { - $this->View->expects($this->at(0)) + $this->View->expects($this->once()) ->method('append') ->with('script', $this->stringContains('https://www.google.com/recaptcha/api.js')); $this->User->addReCaptchaScript(); diff --git a/tests/TestCase/Webauthn/AuthenticateAdapterTest.php b/tests/TestCase/Webauthn/AuthenticateAdapterTest.php new file mode 100644 index 000000000..70c101c90 --- /dev/null +++ b/tests/TestCase/Webauthn/AuthenticateAdapterTest.php @@ -0,0 +1,79 @@ +get('CakeDC/Users.Users'); + $user = $UsersTable->get($userId); + $request = ServerRequestFactory::fromGlobals(); + $request->getSession()->write('Webauthn2fa.User', $user); + $adapter = new AuthenticateAdapter($request); + $options = $adapter->getOptions(); + $this->assertInstanceOf(PublicKeyCredentialRequestOptions::class, $options); + $this->assertSame($options, $request->getSession()->read('Webauthn2fa.authenticateOptions')); + $data = json_decode('{"id":"LFdoCFJTyB82ZzSJUHc-c72yraRc_1mPvGX8ToE8su39xX26Jcqd31LUkKOS36FIAWgWl6itMKqmDvruha6ywA","rawId":"LFdoCFJTyB82ZzSJUHc-c72yraRc_1mPvGX8ToE8su39xX26Jcqd31LUkKOS36FIAWgWl6itMKqmDvruha6ywA","response":{"authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MBAAAAAA","signature":"MEYCIQCv7EqsBRtf2E4o_BjzZfBwNpP8fLjd5y6TUOLWt5l9DQIhANiYig9newAJZYTzG1i5lwP-YQk9uXFnnDaHnr2yCKXL","userHandle":"","clientDataJSON":"eyJjaGFsbGVuZ2UiOiJ4ZGowQ0JmWDY5MnFzQVRweTBrTmM4NTMzSmR2ZExVcHFZUDh3RFRYX1pFIiwiY2xpZW50RXh0ZW5zaW9ucyI6e30sImhhc2hBbGdvcml0aG0iOiJTSEEtMjU2Iiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwIiwidHlwZSI6IndlYmF1dGhuLmdldCJ9"},"type":"public-key"}', true); + $request = $request->withParsedBody($data); + + $adapter = $this->getMockBuilder(AuthenticateAdapter::class) + ->onlyMethods(['loadAndCheckAssertionResponse']) + ->setConstructorArgs([$request]) + ->getMock(); + $publicKeyCredentialId = '12b37486-9299-4331-ac33-85b2d985b6fe'; + $userId = '00000000-0000-0000-0000-000000000001'; + $credentialData = [ + 'publicKeyCredentialId' => $publicKeyCredentialId, + 'type' => 'public-key', + 'transports' => [], + 'attestationType' => 'none', + 'trustPath' => [ + 'type' => 'Webauthn\TrustPath\EmptyTrustPath', + ], + 'aaguid' => '00000000-0000-0000-0000-000000000000', + 'credentialPublicKey' => Base64Url::encode('000000000000000000000000000000000000-9999999999999999999999999999999999999999-XXXXXXXXXXXXX-YYYYYYYYYYY'), + 'userHandle' => Base64Url::encode($userId), + 'counter' => 191, + 'otherUI' => null, + ]; + $credential = PublicKeyCredentialSource::createFromArray($credentialData); + $adapter->expects($this->once()) + ->method('loadAndCheckAssertionResponse') + ->with( + $this->equalTo($options) + ) + ->willReturn($credential); + $actual = $adapter->verifyResponse(); + $this->assertEquals($credential, $actual); + + $adapter = new AuthenticateAdapter($request); + + $this->expectException(\Assert\InvalidArgumentException::class); + $this->getExpectedExceptionMessage('The credential ID is not allowed.'); + $adapter->verifyResponse(); + } +} diff --git a/tests/TestCase/Webauthn/RegisterAdapterTest.php b/tests/TestCase/Webauthn/RegisterAdapterTest.php new file mode 100644 index 000000000..3ce86a8e8 --- /dev/null +++ b/tests/TestCase/Webauthn/RegisterAdapterTest.php @@ -0,0 +1,90 @@ +get('CakeDC/Users.Users'); + $user = $UsersTable->get($userId); + $request = ServerRequestFactory::fromGlobals(); + $request->getSession()->write('Webauthn2fa.User', $user); + $adapter = new RegisterAdapter($request, $UsersTable); + $this->assertFalse($adapter->hasCredential()); + $options = $adapter->getOptions(); + $this->assertInstanceOf(PublicKeyCredentialCreationOptions::class, $options); + $this->assertSame($options, $request->getSession()->read('Webauthn2fa.registerOptions')); + + $data = '{"id":"LFdoCFJTyB82ZzSJUHc-c72yraRc_1mPvGX8ToE8su39xX26Jcqd31LUkKOS36FIAWgWl6itMKqmDvruha6ywA","rawId":"LFdoCFJTyB82ZzSJUHc-c72yraRc_1mPvGX8ToE8su39xX26Jcqd31LUkKOS36FIAWgWl6itMKqmDvruha6ywA","response":{"clientDataJSON":"eyJjaGFsbGVuZ2UiOiJOeHlab3B3VktiRmw3RW5uTWFlXzVGbmlyN1FKN1FXcDFVRlVLakZIbGZrIiwiY2xpZW50RXh0ZW5zaW9ucyI6e30sImhhc2hBbGdvcml0aG0iOiJTSEEtMjU2Iiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwIiwidHlwZSI6IndlYmF1dGhuLmNyZWF0ZSJ9","attestationObject":"o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEcwRQIgVzzvX3Nyp_g9j9f2B-tPWy6puW01aZHI8RXjwqfDjtQCIQDLsdniGPO9iKr7tdgVV-FnBYhvzlZLG3u28rVt10YXfGN4NWOBWQJOMIICSjCCATKgAwIBAgIEVxb3wDANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowLDEqMCgGA1UEAwwhWXViaWNvIFUyRiBFRSBTZXJpYWwgMjUwNTY5MjI2MTc2MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZNkcVNbZV43TsGB4TEY21UijmDqvNSfO6y3G4ytnnjP86ehjFK28-FdSGy9MSZ-Ur3BVZb4iGVsptk5NrQ3QYqM7MDkwIgYJKwYBBAGCxAoCBBUxLjMuNi4xLjQuMS40MTQ4Mi4xLjUwEwYLKwYBBAGC5RwCAQEEBAMCBSAwDQYJKoZIhvcNAQELBQADggEBAHibGMqbpNt2IOL4i4z96VEmbSoid9Xj--m2jJqg6RpqSOp1TO8L3lmEA22uf4uj_eZLUXYEw6EbLm11TUo3Ge-odpMPoODzBj9aTKC8oDFPfwWj6l1O3ZHTSma1XVyPqG4A579f3YAjfrPbgj404xJns0mqx5wkpxKlnoBKqo1rqSUmonencd4xanO_PHEfxU0iZif615Xk9E4bcANPCfz-OLfeKXiT-1msixwzz8XGvl2OTMJ_Sh9G9vhE-HjAcovcHfumcdoQh_WM445Za6Pyn9BZQV3FCqMviRR809sIATfU5lu86wu_5UGIGI7MFDEYeVGSqzpzh6mlcn8QSIZoYXV0aERhdGFYxEmWDeWIDoxodDQXD2R2YFuP5K65ooYyx5lc87qDHZdjQQAAAAAAAAAAAAAAAAAAAAAAAAAAAEAsV2gIUlPIHzZnNIlQdz5zvbKtpFz_WY-8ZfxOgTyy7f3Ffbolyp3fUtSQo5LfoUgBaBaXqK0wqqYO-u6FrrLApQECAyYgASFYIPr9-YH8DuBsOnaI3KJa0a39hyxh9LDtHErNvfQSyxQsIlgg4rAuQQ5uy4VXGFbkiAt0uwgJJodp-DymkoBcrGsLtkI"},"type":"public-key"}'; + $request = $request->withParsedBody( + json_decode($data, true) + ); + //Mock success response + $adapter = $this->getMockBuilder(RegisterAdapter::class) + ->onlyMethods(['loadAndCheckAttestationResponse']) + ->setConstructorArgs([$request]) + ->getMock(); + $publicKeyCredentialId = '12b37486-9299-4331-ac33-85b2d985b6fe'; + $userId = '00000000-0000-0000-0000-000000000002'; + $credentialData = [ + 'publicKeyCredentialId' => $publicKeyCredentialId, + 'type' => 'public-key', + 'transports' => [], + 'attestationType' => 'none', + 'trustPath' => [ + 'type' => 'Webauthn\TrustPath\EmptyTrustPath', + ], + 'aaguid' => '00000000-0000-0000-0000-000000000000', + 'credentialPublicKey' => Base64Url::encode('000000000000000000000000000000000000-9999999999999999999999999999999999999999-XXXXXXXXXXXXX-YYYYYYYYYYY'), + 'userHandle' => Base64Url::encode($userId), + 'counter' => 191, + 'otherUI' => null, + ]; + $credential = PublicKeyCredentialSource::createFromArray($credentialData); + $adapter->expects($this->once()) + ->method('loadAndCheckAttestationResponse') + ->with( + $this->equalTo($options) + ) + ->willReturn($credential); + $credentialsList = $adapter->getUser()->additional_data['webauthn_credentials'] ?? []; + $this->assertCount(0, $credentialsList); + $actual = $adapter->verifyResponse(); + $this->assertEquals($credential, $actual); + $credentialsList = $adapter->getUser()->additional_data['webauthn_credentials']; + $this->assertCount(1, $credentialsList); + $key = key($credentialsList); + $this->assertIsString($key); + $this->assertTrue(isset($credentialsList[$key]['publicKeyCredentialId'])); + $this->assertTrue($adapter->hasCredential()); + //Invalid challenge without mock + $adapter = new RegisterAdapter($request); + $this->expectException(\Assert\InvalidArgumentException::class); + $this->getExpectedExceptionMessage('Invalid challenge.'); + $adapter->verifyResponse(); + } +} diff --git a/tests/TestCase/Webauthn/Repository/UserCredentialSourceRepositoryTest.php b/tests/TestCase/Webauthn/Repository/UserCredentialSourceRepositoryTest.php new file mode 100644 index 000000000..021d08c8b --- /dev/null +++ b/tests/TestCase/Webauthn/Repository/UserCredentialSourceRepositoryTest.php @@ -0,0 +1,122 @@ +get('CakeDC/Users.Users'); + $user = $UsersTable->get('00000000-0000-0000-0000-000000000001'); + $repository = new UserCredentialSourceRepository($user, $UsersTable); + $credential = $repository->findOneByCredentialId('12b37486-9299-4331-ac33-85b2d985b6fe'); + $this->assertInstanceOf(PublicKeyCredentialSource::class, $credential); + + //Not found id + $repository = new UserCredentialSourceRepository($user, $UsersTable); + $credential = $repository->findOneByCredentialId('some-testing-value'); + $this->assertNull($credential); + } + + /** + * Test findAllForUserEntity method + * + * @return void + */ + public function testFindAllForUserEntity() + { + $UsersTable = TableRegistry::getTableLocator()->get('CakeDC/Users.Users'); + $userId = '00000000-0000-0000-0000-000000000001'; + $user = $UsersTable->get($userId); + $userEntity = new PublicKeyCredentialUserEntity( + 'john.doe', + $userId, + 'John Doe' + ); + $repository = new UserCredentialSourceRepository($user, $UsersTable); + $credentials = $repository->findAllForUserEntity($userEntity); + $this->assertCount(1, $credentials); + $this->assertInstanceOf(PublicKeyCredentialSource::class, $credentials[0]); + + //Not found id + $userEntityInvalid = new PublicKeyCredentialUserEntity( + 'john.doe', + '00000000-0000-0000-0000-000000000004', + 'John Doe' + ); + $repository = new UserCredentialSourceRepository($user, $UsersTable); + $credentials = $repository->findAllForUserEntity($userEntityInvalid); + $this->assertEmpty($credentials); + } + + /** + * Test saveCredentialSource method + * + * @return void + */ + public function testSaveCredentialSource() + { + \Cake\Database\TypeFactory::map('json', \Cake\Database\Type\JsonType::class); + + $publicKeyCredentialId = '12b37486-9299-4331-ac33-85b2d985b6fe'; + $userId = '00000000-0000-0000-0000-000000000001'; + $credentialData = [ + 'publicKeyCredentialId' => $publicKeyCredentialId, + 'type' => 'public-key', + 'transports' => [], + 'attestationType' => 'none', + 'trustPath' => [ + 'type' => 'Webauthn\TrustPath\EmptyTrustPath', + ], + 'aaguid' => '00000000-0000-0000-0000-000000000000', + 'credentialPublicKey' => Base64Url::encode('000000000000000000000000000000000000-9999999999999999999999999999999999999999-XXXXXXXXXXXXX-YYYYYYYYYYY'), + 'userHandle' => Base64Url::encode($userId), + 'counter' => 191, + 'otherUI' => null, + ]; + $UsersTable = TableRegistry::getTableLocator()->get('CakeDC/Users.Users'); + $user = $UsersTable->get($userId); + $firstKey = key($user->additional_data['webauthn_credentials']); + $userEntity = new PublicKeyCredentialUserEntity( + 'john.doe', + $userId, + 'John Doe' + ); + $publicKey = PublicKeyCredentialSource::createFromArray($credentialData); + $repository = new UserCredentialSourceRepository($user, $UsersTable); + $repository->saveCredentialSource($publicKey); + $credentials = $repository->findAllForUserEntity($userEntity); + $this->assertCount(2, $credentials); + $userAfter = $UsersTable->get($user->id); + $this->assertArrayHasKey( + '12b37486-9299-4331-ac33-85b2d985b6fe', + $userAfter->additional_data['webauthn_credentials'] + ); + $this->assertArrayHasKey( + $firstKey, + $userAfter->additional_data['webauthn_credentials'] + ); + $this->assertCount( + 2, + $userAfter->additional_data['webauthn_credentials'] + ); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index e8575c540..f5424bd41 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -29,7 +29,7 @@ return $root; } } while ($root !== $lastRoot); - throw new Exception("Cannot find the root of the application, unable to run tests"); + throw new Exception('Cannot find the root of the application, unable to run tests'); }; $root = $findRoot(__FILE__); unset($findRoot); @@ -118,7 +118,7 @@ class_alias('TestApp\Controller\AppController', 'App\Controller\AppController'); 'dir' => 'src', 'webroot' => WEBROOT_DIR, 'wwwRoot' => WWW_ROOT, - 'fullBaseUrl' => 'http://localhost', + 'fullBaseUrl' => 'http://example.com', 'imageBaseUrl' => 'img/', 'jsBaseUrl' => 'js/', 'cssBaseUrl' => 'css/', @@ -131,3 +131,13 @@ class_alias('TestApp\Controller\AppController', 'App\Controller\AppController'); Plugin::getCollection()->add(new \CakeDC\Users\Plugin()); session_id('cli'); + +\Cake\Core\Configure::write('Users.AllowedRedirectHosts', [ + 'localhost', + 'example.com', +]); + +if (env('FIXTURE_SCHEMA_METADATA')) { + $loader = new \Cake\TestSuite\Fixture\SchemaLoader(); + $loader->loadInternalFile(env('FIXTURE_SCHEMA_METADATA')); +} diff --git a/tests/schema.php b/tests/schema.php new file mode 100644 index 000000000..18db1b7c5 --- /dev/null +++ b/tests/schema.php @@ -0,0 +1,95 @@ + 'posts', + 'columns' => [ + 'id' => ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null], + 'title' => ['type' => 'string', 'length' => 255, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], + 'user_id' => ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], + ], + 'constraints' => [ + 'primary' => ['type' => 'primary', 'columns' => ['id'], 'length' => []], + ], + ], + [ + 'table' => 'social_accounts', + 'columns' => [ + 'id' => ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null], + 'user_id' => ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], + 'provider' => ['type' => 'string', 'length' => 255, 'unsigned' => false, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'autoIncrement' => null], + 'username' => ['type' => 'string', 'length' => 255, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], + 'reference' => ['type' => 'string', 'length' => 255, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], + 'avatar' => ['type' => 'string', 'length' => 255, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], + 'description' => ['type' => 'text', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], + 'link' => ['type' => 'text', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], + 'token' => ['type' => 'string', 'length' => 500, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], + 'token_secret' => ['type' => 'string', 'length' => 500, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], + 'token_expires' => ['type' => 'datetime', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], + 'active' => ['type' => 'boolean', 'length' => null, 'null' => false, 'default' => true, 'comment' => '', 'precision' => null], + 'data' => ['type' => 'text', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null], + 'created' => ['type' => 'datetime', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null], + 'modified' => ['type' => 'datetime', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null], + ], + 'constraints' => [ + 'primary' => [ + 'type' => 'primary', + 'columns' => [ + 'id', + ], + ], + ], + ], + [ + 'table' => 'users', + 'columns' => [ + 'id' => ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null], + 'username' => ['type' => 'string', 'length' => 255, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], + 'email' => ['type' => 'string', 'length' => 255, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], + 'password' => ['type' => 'string', 'length' => 255, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], + 'first_name' => ['type' => 'string', 'length' => 50, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], + 'last_name' => ['type' => 'string', 'length' => 50, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], + 'token' => ['type' => 'string', 'length' => 255, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], + 'token_expires' => ['type' => 'datetime', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], + 'api_token' => ['type' => 'string', 'length' => 255, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], + 'activation_date' => ['type' => 'datetime', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], + 'secret' => ['type' => 'string', 'length' => 255, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], + 'secret_verified' => ['type' => 'boolean', 'length' => null, 'null' => true, 'default' => false, 'comment' => '', 'precision' => null], + 'tos_date' => ['type' => 'datetime', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], + 'active' => ['type' => 'boolean', 'length' => null, 'null' => false, 'default' => true, 'comment' => '', 'precision' => null], + 'is_superuser' => ['type' => 'boolean', 'length' => null, 'unsigned' => false, 'null' => false, 'default' => false, 'comment' => '', 'precision' => null, 'autoIncrement' => null], + 'role' => ['type' => 'string', 'length' => 255, 'null' => true, 'default' => 'user', 'comment' => '', 'precision' => null, 'fixed' => null], + 'created' => ['type' => 'datetime', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null], + 'modified' => ['type' => 'datetime', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null], + 'additional_data' => ['type' => 'text', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], + 'last_login' => ['type' => 'datetime', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], + ], + 'constraints' => [ + 'primary' => [ + 'type' => 'primary', + 'columns' => [ + 'id', + ], + ], + ], + ], + [ + 'table' => 'posts_users', + 'columns' => [ + 'id' => ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null], + 'user_id' => ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], + 'post_id' => ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], + ], + 'constraints' => [ + 'primary' => ['type' => 'primary', 'columns' => ['id'], 'length' => []], + ], + ], +]; diff --git a/tests/test_app/TestApp/Controller/AppController.php b/tests/test_app/TestApp/Controller/AppController.php index 947d6a0c4..db11ec36d 100644 --- a/tests/test_app/TestApp/Controller/AppController.php +++ b/tests/test_app/TestApp/Controller/AppController.php @@ -21,7 +21,6 @@ * * Add your application-wide methods in the class below, your controllers * will inherit them. - * */ class AppController extends Controller { diff --git a/tests/test_app/TestApp/Mailer/OverrideMailer.php b/tests/test_app/TestApp/Mailer/OverrideMailer.php index 792001a4f..3dbe44fe8 100644 --- a/tests/test_app/TestApp/Mailer/OverrideMailer.php +++ b/tests/test_app/TestApp/Mailer/OverrideMailer.php @@ -18,12 +18,12 @@ /** * Override default mailer class to test customization - * */ class OverrideMailer extends UsersMailer { /** * Override the resetPassword email with a custom template and subject + * * @param EntityInterface $user * @return array|void */ diff --git a/webroot/js/webauthn.js b/webroot/js/webauthn.js new file mode 100644 index 000000000..45f9e9c22 --- /dev/null +++ b/webroot/js/webauthn.js @@ -0,0 +1,214 @@ +//Source code based on https://github.com/web-auth/webauthn-helper +// Predefined fetch function +const fetchEndpoint = (data, url, header) => { + return fetch( + url, + { + method: 'POST', + credentials: 'same-origin', + redirect: 'error', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + ...header + }, + body: JSON.stringify(data), + } + ); +} + +// Decodes a Base64Url string +const base64UrlDecode = (input) => { + input = input + .replace(/-/g, '+') + .replace(/_/g, '/'); + + const pad = input.length % 4; + if (pad) { + if (pad === 1) { + throw new Error('InvalidLengthError: Input base64url string is the wrong length to determine padding'); + } + input += new Array(5-pad).join('='); + } + + return window.atob(input); +}; + +// Converts an array of bytes into a Base64Url string +const arrayToBase64String = (a) => btoa(String.fromCharCode(...a)); + +// Prepares the public key options object returned by the Webauthn Framework +const preparePublicKeyOptions = publicKey => { + //Convert challenge from Base64Url string to Uint8Array + publicKey.challenge = Uint8Array.from( + base64UrlDecode(publicKey.challenge), + c => c.charCodeAt(0) + ); + + //Convert the user ID from Base64 string to Uint8Array + if (publicKey.user !== undefined) { + publicKey.user = { + ...publicKey.user, + id: Uint8Array.from( + window.atob(publicKey.user.id), + c => c.charCodeAt(0) + ), + }; + } + + //If excludeCredentials is defined, we convert all IDs to Uint8Array + if (publicKey.excludeCredentials !== undefined) { + publicKey.excludeCredentials = publicKey.excludeCredentials.map( + data => { + return { + ...data, + id: Uint8Array.from( + base64UrlDecode(data.id), + c => c.charCodeAt(0) + ), + }; + } + ); + } + + if (publicKey.allowCredentials !== undefined) { + publicKey.allowCredentials = publicKey.allowCredentials.map( + data => { + return { + ...data, + id: Uint8Array.from( + base64UrlDecode(data.id), + c => c.charCodeAt(0) + ), + }; + } + ); + } + + return publicKey; +}; + +// Prepares the public key credentials object returned by the authenticator +const preparePublicKeyCredentials = data => { + const publicKeyCredential = { + id: data.id, + type: data.type, + rawId: arrayToBase64String(new Uint8Array(data.rawId)), + response: { + clientDataJSON: arrayToBase64String( + new Uint8Array(data.response.clientDataJSON) + ), + }, + }; + + if (data.response.attestationObject !== undefined) { + publicKeyCredential.response.attestationObject = arrayToBase64String( + new Uint8Array(data.response.attestationObject) + ); + } + + if (data.response.authenticatorData !== undefined) { + publicKeyCredential.response.authenticatorData = arrayToBase64String( + new Uint8Array(data.response.authenticatorData) + ); + } + + if (data.response.signature !== undefined) { + publicKeyCredential.response.signature = arrayToBase64String( + new Uint8Array(data.response.signature) + ); + } + + if (data.response.userHandle !== undefined) { + publicKeyCredential.response.userHandle = arrayToBase64String( + new Uint8Array(data.response.userHandle) + ); + } + + return publicKeyCredential; +}; + +const useLogin = ({actionUrl = '/login', actionHeader = {}, optionsUrl = '/login/options'}, optionsHeader = {}) => { + return async (data) => { + const optionsResponse = await fetchEndpoint(data, optionsUrl, optionsHeader); + const json = await optionsResponse.json(); + const publicKey = preparePublicKeyOptions(json); + const credentials = await navigator.credentials.get({publicKey}); + const publicKeyCredential = preparePublicKeyCredentials(credentials); + const actionResponse = await fetchEndpoint(publicKeyCredential, actionUrl, actionHeader); + if (! actionResponse.ok) { + throw actionResponse; + } + const responseBody = await actionResponse.text(); + + return responseBody !== '' ? JSON.parse(responseBody) : responseBody; + }; +}; + + +const useRegistration = ({actionUrl = '/register', actionHeader = {}, optionsUrl = '/register/options'}, optionsHeader = {}) => { + return async (data) => { + const optionsResponse = await fetchEndpoint(data, optionsUrl, optionsHeader); + const json = await optionsResponse.json(); + const publicKey = preparePublicKeyOptions(json); + const credentials = await navigator.credentials.create({publicKey}); + const publicKeyCredential = preparePublicKeyCredentials(credentials); + const actionResponse = await fetchEndpoint(publicKeyCredential, actionUrl, actionHeader); + if (! actionResponse.ok) { + throw actionResponse; + } + const responseBody = await actionResponse.text(); + + return responseBody !== '' ? JSON.parse(responseBody) : responseBody; + }; +}; + +var Webauthn2faHelper = { + toggleElem: function(options) { + console.log({options}); + document.getElementById(options.authenticateElemId).style.display = options.isRegister ? 'none' : 'block'; + document.getElementById(options.registerElemId).style.display = options.isRegister ? 'block' : 'none'; + }, + authenticate: function(options) { + var authenticate = useLogin({ + actionUrl: options.authenticateActionUrl, + optionsUrl: options.authenticateOptionsUrl + }); + authenticate({ + username: options.username + }) + .then(function (response) { + window.location.href = response.redirectUrl; + }) + .catch((error) => { + window.alert('Authentication failure'); + }) + }, + register: function(options) { + var register = useRegistration({ + actionUrl: options.registerActionUrl, + optionsUrl: options.registerOptionsUrl + }); + + return register({ + username: options.username, + }) + .catch((error) => { + window.alert('Registration failed'); + }); + }, + run: function(options) { + Webauthn2faHelper.toggleElem(options); + if (options.isRegister) { + return Webauthn2faHelper.register(options) + .then(function (response) { + if (response && response.success) { + options.isRegister = false; + Webauthn2faHelper.toggleElem(options); + Webauthn2faHelper.authenticate(options); + } + }); + } + return Webauthn2faHelper.authenticate(options); + } +}
= __d('cake_d_c/users', 'When the YubiKey starts blinking, press the golden disc to activate it. Depending on the web browser you might need to confirm the use of extended information from the YubiKey.')?>
= $this->Html->link( __d('cake_d_c/users', 'Reload'), diff --git a/templates/Users/webauthn2fa.php b/templates/Users/webauthn2fa.php new file mode 100644 index 000000000..3c2b7c898 --- /dev/null +++ b/templates/Users/webauthn2fa.php @@ -0,0 +1,56 @@ +Html->script('CakeDC/Users.webauthn.js', ['block' => true]); +$this->assign('title', __d('cake_d_c/users','Two-factor authentication')); +?> +
= __d('cake_d_c/users', 'In order to enable your YubiKey the first step is to perform a registration.')?>
= __d('cake_d_c/users', 'You can now finish the authentication process using the registered device.')?>
= $this->Html->link( + __d('cake_d_c/users','Reload'), + ['action' => 'webauthn2fa'], + ['class' => 'btn btn-primary'] + )?>