diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb98b5723..032c79a25 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - php-version: ['7.3', '7.4', '8.0'] + php-version: ['7.3', '7.4', '8.0', '8.1'] db-type: [sqlite, mysql, pgsql] prefer-lowest: [''] diff --git a/.semver b/.semver index a0290e8f4..726c4d95e 100644 --- a/.semver +++ b/.semver @@ -1,5 +1,5 @@ --- -:major: 9 -:minor: 2 +:major: 11 +:minor: 0 :patch: 0 :special: '' diff --git a/CHANGELOG.md b/CHANGELOG.md index d8eea9d31..78c464692 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ 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 diff --git a/Docs/Documentation/Authentication.md b/Docs/Documentation/Authentication.md index 74d2c3c1f..9d692dd21 100644 --- a/Docs/Documentation/Authentication.md +++ b/Docs/Documentation/Authentication.md @@ -205,5 +205,5 @@ class AppAuthenticationServiceLoader extends AuthenticationServiceLoader - Add this to your config/users.php file to change the authentication service loader: ```php -'Auth.Authentication.serviceLoader' => \App\Loader\AuthenticationServiceLoader::class, +'Auth.Authentication.serviceLoader' => \App\Loader\AppAuthenticationServiceLoader::class, ``` diff --git a/Docs/Documentation/Configuration.md b/Docs/Documentation/Configuration.md index 080adb67f..a7118a99b 100644 --- a/Docs/Documentation/Configuration.md +++ b/Docs/Documentation/Configuration.md @@ -105,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 @@ -172,6 +174,7 @@ user identify. Add this to your config/users.php: ```php '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". diff --git a/Docs/Documentation/Migration/8.x-9.0.md b/Docs/Documentation/Migration/8.x-9.0.md index 7ae2a134c..c375d89cd 100644 --- a/Docs/Documentation/Migration/8.x-9.0.md +++ b/Docs/Documentation/Migration/8.x-9.0.md @@ -111,3 +111,21 @@ if ($user) { //Do stuff } ``` + +Settting user data in session for unit tests +-------------------------------------------- + +Authentication process read and store user data at session key 'Auth' not 'Auth.User' and the +value should be an User object. + +In your integration test class replace this: + +``` +$this->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/Translations.md b/Docs/Documentation/Translations.md index 13f54f404..d9cc74def 100644 --- a/Docs/Documentation/Translations.md +++ b/Docs/Documentation/Translations.md @@ -13,6 +13,6 @@ The Plugin is translated into several languages: * 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/README.md b/README.md index 3b8ddb439..fbd5af5f4 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,8 @@ Versions and branches | CakePHP | CakeDC Users Plugin | Tag | Notes | | :-------------: | :------------------------: | :--: | :---- | -| ^4.0 | [master](https://github.com/cakedc/users/tree/master) | 9.0.5 | 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 <4.0 | [develop](https://github.com/cakedc/users/tree/develop) | - | unstable | diff --git a/composer.json b/composer.json index 7395b49fa..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", - "cakedc/auth": "^6.0.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 3e9bc2369..5c52467fb 100644 --- a/config/permissions.php +++ b/config/permissions.php @@ -79,6 +79,11 @@ 'u2fRegisterFinish', 'u2fAuthenticate', 'u2fAuthenticateFinish', + 'webauthn2fa', + 'webauthn2faRegister', + 'webauthn2faRegisterOptions', + 'webauthn2faAuthenticate', + 'webauthn2faAuthenticateOptions', ], 'bypassAuth' => true, ], @@ -134,6 +139,6 @@ '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 e927ea4d4..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 @@ -94,10 +114,12 @@ 'Config' => [ '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' => [ @@ -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 146e9ed85..52b966521 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -3,265 +3,265 @@ parameters: - 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 + count: 2 + 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: "#^Access to an undefined property CakeDC\\\\Users\\\\Controller\\\\UsersController\\:\\:\\$Authentication\\.$#" + count: 2 + path: src/Controller/UsersController.php - - message: "#^Call to an undefined method CakeDC\\\\Users\\\\Model\\\\Table\\\\SocialAccountsTable\\:\\:resendValidation\\(\\)\\.$#" - count: 1 - path: src\Controller\SocialAccountsController.php + message: "#^Access to an undefined property CakeDC\\\\Users\\\\Controller\\\\UsersController\\:\\:\\$OneTimePasswordAuthenticator\\.$#" + count: 3 + path: src/Controller/UsersController.php - - message: "#^Call to an undefined method Cake\\\\ORM\\\\Table\\:\\:linkSocialAccount\\(\\)\\.$#" + 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\\\\Controller\\\\Component\\:\\:handleLogin\\(\\)\\.$#" count: 3 - path: src\Controller\UsersController.php - - - - message: "#^Access to an undefined property CakeDC\\\\Users\\\\Controller\\\\UsersController\\:\\:\\$Authentication\\.$#" - count: 2 - 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: "#^Call to an undefined method Cake\\\\ORM\\\\Table\\:\\:validationPasswordConfirm\\(\\)\\.$#" + 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\\:\\:validationCurrentPassword\\(\\)\\.$#" + 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\\:\\:changePassword\\(\\)\\.$#" - count: 1 - 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\\.$#" + message: "#^Call to an undefined method Cake\\\\ORM\\\\Table\\:\\:validate\\(\\)\\.$#" + count: 2 + path: src/Controller/UsersController.php + + - + message: "#^Call to an undefined method Cake\\\\ORM\\\\Table\\:\\:validationCurrentPassword\\(\\)\\.$#" count: 1 - path: src\Controller\UsersController.php + 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\\:\\: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 + 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\\:\\:success\\(\\) expects array, string given\\.$#" + count: 1 + 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 - - - - message: "#^Access to an undefined property Cake\\\\ORM\\\\Table\\:\\:\\$SocialAccounts\\.$#" - count: 5 - path: src\Model\Behavior\LinkSocialBehavior.php + path: src/Middleware/SocialEmailMiddleware.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 Cake\\\\Datasource\\\\EntityInterface\\:\\:\\$validated\\.$#" + message: "#^Call to an undefined method Cake\\\\ORM\\\\Table\\:\\:findByUsernameOrEmail\\(\\)\\.$#" count: 1 - path: src\Model\Behavior\RegisterBehavior.php + path: src/Model/Behavior/PasswordBehavior.php - - message: "#^Access to an undefined property Cake\\\\ORM\\\\Table\\:\\:\\$isValidateEmail\\.$#" + 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: "#^Cannot call method tokenExpired\\(\\) on array\\|Cake\\\\Datasource\\\\EntityInterface\\.$#" + 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\\\\Datasource\\\\EntityInterface\\:\\:\\$active\\.$#" 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\\\\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\\:\\:\\$token_expires\\.$#" + 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\\\\ORM\\\\Table\\:\\:\\$isValidateEmail\\.$#" + count: 1 + path: src/Model/Behavior/RegisterBehavior.php - 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: "#^Cannot access property \\$token on array\\|Cake\\\\Datasource\\\\EntityInterface\\.$#" + message: "#^Cannot call method tokenExpired\\(\\) on array\\|Cake\\\\Datasource\\\\EntityInterface\\.$#" + count: 1 + path: src/Model/Behavior/RegisterBehavior.php + + - + 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 + path: src/Shell/UsersShell.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 - - 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 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/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 cb3e7d1c0..9f3cef77c 100644 --- a/src/Authenticator/SocialAuthTrait.php +++ b/src/Authenticator/SocialAuthTrait.php @@ -40,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/Component/LoginComponent.php b/src/Controller/Component/LoginComponent.php index ad75f1b9d..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); } @@ -146,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); @@ -184,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/OneTimePasswordVerifyTrait.php b/src/Controller/Traits/OneTimePasswordVerifyTrait.php index da3a963d8..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')) { diff --git a/src/Controller/Traits/PasswordManagementTrait.php b/src/Controller/Traits/PasswordManagementTrait.php index 431d04be0..09878ae2a 100644 --- a/src/Controller/Traits/PasswordManagementTrait.php +++ b/src/Controller/Traits/PasswordManagementTrait.php @@ -41,6 +41,8 @@ trait PasswordManagementTrait 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; @@ -85,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, @@ -123,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 d2edc6586..a67ac991c 100644 --- a/src/Controller/Traits/ProfileTrait.php +++ b/src/Controller/Traits/ProfileTrait.php @@ -59,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/RegisterTrait.php b/src/Controller/Traits/RegisterTrait.php index 49b43d3be..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() { @@ -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/U2fTrait.php b/src/Controller/Traits/U2fTrait.php index b19370f76..a469a9ce8 100644 --- a/src/Controller/Traits/U2fTrait.php +++ b/src/Controller/Traits/U2fTrait.php @@ -55,7 +55,6 @@ public function u2f() 'action' => 'login', ]); } - if (!$data['registration']) { return $this->redirectWithQuery([ 'action' => 'u2fRegister', @@ -85,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; } @@ -146,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/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 6cffb4590..096bbc9c1 100644 --- a/src/Controller/UsersController.php +++ b/src/Controller/UsersController.php @@ -22,6 +22,7 @@ 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 @@ -40,6 +41,7 @@ class UsersController extends AppController use SimpleCrudTrait; use SocialTrait; use U2fTrait; + use Webauthn2faTrait; /** * Initialize @@ -52,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 d938a749e..4cef96afd 100644 --- a/src/Exception/AccountAlreadyActiveException.php +++ b/src/Exception/AccountAlreadyActiveException.php @@ -13,9 +13,7 @@ namespace CakeDC\Users\Exception; -use Cake\Core\Exception\Exception; - -class AccountAlreadyActiveException extends Exception +class AccountAlreadyActiveException extends \Cake\Core\Exception\CakeException { /** * AccountAlreadyActiveException constructor. diff --git a/src/Exception/AccountNotActiveException.php b/src/Exception/AccountNotActiveException.php index 1d9846a10..a7d1f3741 100644 --- a/src/Exception/AccountNotActiveException.php +++ b/src/Exception/AccountNotActiveException.php @@ -13,9 +13,7 @@ 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'; diff --git a/src/Exception/MissingEmailException.php b/src/Exception/MissingEmailException.php index fb54ddf9f..6ada67bc6 100644 --- a/src/Exception/MissingEmailException.php +++ b/src/Exception/MissingEmailException.php @@ -13,9 +13,7 @@ namespace CakeDC\Users\Exception; -use Cake\Core\Exception\Exception; - -class MissingEmailException extends Exception +class MissingEmailException extends \Cake\Core\Exception\CakeException { /** * MissingEmailException constructor. 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 1a40617f0..ee036ecac 100644 --- a/src/Exception/TokenExpiredException.php +++ b/src/Exception/TokenExpiredException.php @@ -13,9 +13,7 @@ namespace CakeDC\Users\Exception; -use Cake\Core\Exception\Exception; - -class TokenExpiredException extends Exception +class TokenExpiredException extends \Cake\Core\Exception\CakeException { /** * TokenExpiredException constructor. diff --git a/src/Exception/UserAlreadyActiveException.php b/src/Exception/UserAlreadyActiveException.php index d7a62c921..3490eae6d 100644 --- a/src/Exception/UserAlreadyActiveException.php +++ b/src/Exception/UserAlreadyActiveException.php @@ -13,9 +13,7 @@ namespace CakeDC\Users\Exception; -use Cake\Core\Exception\Exception; - -class UserAlreadyActiveException extends Exception +class UserAlreadyActiveException extends \Cake\Core\Exception\CakeException { /** * UserAlreadyActiveException constructor. diff --git a/src/Exception/UserNotActiveException.php b/src/Exception/UserNotActiveException.php index e82c41229..5533b4447 100644 --- a/src/Exception/UserNotActiveException.php +++ b/src/Exception/UserNotActiveException.php @@ -13,9 +13,7 @@ namespace CakeDC\Users\Exception; -use Cake\Core\Exception\Exception; - -class UserNotActiveException extends Exception +class UserNotActiveException extends \Cake\Core\Exception\CakeException { /** * UserNotActiveException constructor. diff --git a/src/Exception/WrongPasswordException.php b/src/Exception/WrongPasswordException.php index 79a6979ff..8a4ec0b56 100644 --- a/src/Exception/WrongPasswordException.php +++ b/src/Exception/WrongPasswordException.php @@ -13,9 +13,7 @@ namespace CakeDC\Users\Exception; -use Cake\Core\Exception\Exception; - -class WrongPasswordException extends Exception +class WrongPasswordException extends \Cake\Core\Exception\CakeException { /** * WrongPasswordException constructor. diff --git a/src/Identifier/SocialIdentifier.php b/src/Identifier/SocialIdentifier.php index 98248aee6..7a75ecdc4 100644 --- a/src/Identifier/SocialIdentifier.php +++ b/src/Identifier/SocialIdentifier.php @@ -56,12 +56,10 @@ 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(); } /** diff --git a/src/Loader/AuthenticationServiceLoader.php b/src/Loader/AuthenticationServiceLoader.php index 5ef982860..c4d2b6958 100644 --- a/src/Loader/AuthenticationServiceLoader.php +++ b/src/Loader/AuthenticationServiceLoader.php @@ -106,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 3d4b05bee..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); diff --git a/src/Loader/MiddlewareQueueLoader.php b/src/Loader/MiddlewareQueueLoader.php index 6b943870a..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,16 +41,20 @@ 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); } /** @@ -71,12 +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); } @@ -100,15 +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 0091880b5..2e4070e96 100644 --- a/src/Mailer/UsersMailer.php +++ b/src/Mailer/UsersMailer.php @@ -107,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 9abe7fc08..00ca65130 100644 --- a/src/Middleware/SocialAuthMiddleware.php +++ b/src/Middleware/SocialAuthMiddleware.php @@ -41,7 +41,7 @@ class SocialAuthMiddleware implements MiddlewareInterface */ 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')); diff --git a/src/Middleware/UnauthorizedHandler/DefaultRedirectHandler.php b/src/Middleware/UnauthorizedHandler/DefaultRedirectHandler.php index 5681be54c..709a3fe43 100644 --- a/src/Middleware/UnauthorizedHandler/DefaultRedirectHandler.php +++ b/src/Middleware/UnauthorizedHandler/DefaultRedirectHandler.php @@ -107,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/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 aafc0690a..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; /** @@ -124,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 52fb92a55..e307d15a0 100644 --- a/src/Model/Behavior/PasswordBehavior.php +++ b/src/Model/Behavior/PasswordBehavior.php @@ -62,10 +62,8 @@ public function resetToken($reference, array $options = []) $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); diff --git a/src/Model/Behavior/RegisterBehavior.php b/src/Model/Behavior/RegisterBehavior.php index 01bc06623..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; @@ -64,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; @@ -125,9 +131,8 @@ public function activateUser(EntityInterface $user) $user->activation_date = new \DateTime(); $user->token_expires = null; $user->active = true; - $result = $this->_table->save($user); - return $result; + return $this->_table->save($user); } /** @@ -138,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 271533403..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')); } /** @@ -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 6c5c2fc02..7911ed802 100644 --- a/src/Model/Behavior/SocialBehavior.php +++ b/src/Model/Behavior/SocialBehavior.php @@ -135,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); @@ -150,9 +150,8 @@ protected function _createSocialUser($data, $options = []) } $this->_table->isValidateEmail = $validateEmail; - $result = $this->_table->save($user); - return $result; + return $this->_table->save($user); } /** @@ -169,6 +168,7 @@ protected function _createSocialUser($data, $options = []) */ protected function _populateUser($data, $existingUser, $useEmail, $validateEmail, $tokenExpiration) { + $userData = []; $accountData = $this->extractAccountData($data); $accountData['active'] = true; @@ -247,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; } diff --git a/src/Model/Entity/User.php b/src/Model/Entity/User.php index 27ef8af10..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,10 +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 string $api_token - * @property array $additional_data + * @property array|string $additional_data * @property \CakeDC\Users\Model\Entity\SocialAccount[] $social_accounts */ class User extends Entity @@ -81,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; @@ -111,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(); @@ -142,7 +142,7 @@ public function tokenExpired() return true; } - return new Time($this->token_expires) < Time::now(); + return new FrozenTime($this->token_expires) < FrozenTime::now(); } /** @@ -167,12 +167,15 @@ 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; } /** @@ -183,7 +186,7 @@ protected function _getU2fRegistration() */ 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 089f88a9f..4e6954348 100644 --- a/src/Model/Table/UsersTable.php +++ b/src/Model/Table/UsersTable.php @@ -83,10 +83,7 @@ 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'); } /** @@ -186,9 +183,8 @@ public function validationDefault(Validator $validator): 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 9599b1c62..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. * @@ -44,29 +43,6 @@ class Plugin extends BasePlugin implements AuthenticationServiceProviderInterfac 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 */ @@ -74,36 +50,10 @@ 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 146eb2ee8..bd38e2e44 100644 --- a/src/Shell/UsersShell.php +++ b/src/Shell/UsersShell.php @@ -321,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']) ? @@ -387,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/Utility/UsersUrl.php b/src/Utility/UsersUrl.php index 96d05151a..f4c2f9458 100644 --- a/src/Utility/UsersUrl.php +++ b/src/Utility/UsersUrl.php @@ -74,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]; } /** @@ -124,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/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 @@

-

+

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')); +?> +

+
+
+
+ + Flash->render('auth') ?> + Flash->render() ?> +
+ + +

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(); +}, 1000); +Html->scriptEnd();?> diff --git a/tests/Fixture/PostsFixture.php b/tests/Fixture/PostsFixture.php index eb5fb9b7b..4057cbbb8 100644 --- a/tests/Fixture/PostsFixture.php +++ b/tests/Fixture/PostsFixture.php @@ -18,26 +18,6 @@ */ 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 84cc2f1f8..5f5350e9e 100644 --- a/tests/Fixture/PostsUsersFixture.php +++ b/tests/Fixture/PostsUsersFixture.php @@ -18,26 +18,6 @@ */ 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 6200866f5..9789d2c45 100644 --- a/tests/Fixture/SocialAccountsFixture.php +++ b/tests/Fixture/SocialAccountsFixture.php @@ -18,38 +18,6 @@ */ 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 607393d18..d48f6dc55 100644 --- a/tests/Fixture/UsersFixture.php +++ b/tests/Fixture/UsersFixture.php @@ -12,6 +12,7 @@ namespace CakeDC\Users\Test\Fixture; +use Base64Url\Base64Url; use Cake\TestSuite\Fixture\TestFixture; /** @@ -19,42 +20,6 @@ */ 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 * @@ -82,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', @@ -111,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/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/Traits/Integration/LoginTraitIntegrationTest.php b/tests/TestCase/Controller/Traits/Integration/LoginTraitIntegrationTest.php index 982806ea1..9bf8d74d4 100644 --- a/tests/TestCase/Controller/Traits/Integration/LoginTraitIntegrationTest.php +++ b/tests/TestCase/Controller/Traits/Integration/LoginTraitIntegrationTest.php @@ -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(''); $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(''); $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(''); } @@ -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/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(''); + $this->assertResponseContains(''); $this->assertResponseContains(''); } @@ -83,7 +83,7 @@ public function testRegisterPostWithErrors() $this->assertResponseContains(''); $this->assertResponseContains(''); $this->assertResponseContains(''); - $this->assertResponseContains(''); + $this->assertResponseContains(''); $this->assertResponseContains(''); } diff --git a/tests/TestCase/Controller/Traits/Integration/SimpleCrudTraitIntegrationTest.php b/tests/TestCase/Controller/Traits/Integration/SimpleCrudTraitIntegrationTest.php index 90df2fcc2..9b606146e 100644 --- a/tests/TestCase/Controller/Traits/Integration/SimpleCrudTraitIntegrationTest.php +++ b/tests/TestCase/Controller/Traits/Integration/SimpleCrudTraitIntegrationTest.php @@ -95,9 +95,9 @@ public function testCrud() $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'); 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 f6174ab00..683bfc0c7 100644 --- a/tests/TestCase/Controller/Traits/OneTimePasswordVerifyTraitTest.php +++ b/tests/TestCase/Controller/Traits/OneTimePasswordVerifyTraitTest.php @@ -133,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')); @@ -175,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')); @@ -238,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/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 055940d13..dc792ee49 100644 --- a/tests/TestCase/Controller/Traits/SimpleCrudTraitTest.php +++ b/tests/TestCase/Controller/Traits/SimpleCrudTraitTest.php @@ -152,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([ @@ -185,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([ @@ -218,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([ @@ -248,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 eee53e676..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']], ]; } 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 1b763b16b..9cebd1f89 100644 --- a/tests/TestCase/Controller/UsersControllerTest.php +++ b/tests/TestCase/Controller/UsersControllerTest.php @@ -85,7 +85,7 @@ public function testUnauthorizedRedirectLogged() $this->session(['Auth' => $user]); $this->configRequest([ 'headers' => [ - 'REFERER' => 'http://localhost/profile', + 'REFERER' => 'http://example.com/profile', ], ]); $this->get('/users/index'); diff --git a/tests/TestCase/Middleware/SocialAuthMiddlewareTest.php b/tests/TestCase/Middleware/SocialAuthMiddlewareTest.php index b09099ce1..6bbb8e28d 100644 --- a/tests/TestCase/Middleware/SocialAuthMiddlewareTest.php +++ b/tests/TestCase/Middleware/SocialAuthMiddlewareTest.php @@ -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/LinkSocialBehaviorTest.php b/tests/TestCase/Model/Behavior/LinkSocialBehaviorTest.php index ebc292e19..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 @@ -101,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 [ @@ -195,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 [ @@ -303,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/RegisterBehaviorTest.php b/tests/TestCase/Model/Behavior/RegisterBehaviorTest.php index da7a640da..317cc6a80 100644 --- a/tests/TestCase/Model/Behavior/RegisterBehaviorTest.php +++ b/tests/TestCase/Model/Behavior/RegisterBehaviorTest.php @@ -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', @@ -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', @@ -213,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', 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 7aa6b9d17..58b1e7fd6 100644 --- a/tests/TestCase/Model/Table/UsersTableTest.php +++ b/tests/TestCase/Model/Table/UsersTableTest.php @@ -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', @@ -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', 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/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 212a3b467..f5424bd41 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -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/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); + } +}