diff --git a/CHANGELOG.md b/CHANGELOG.md index 9461dd5c3..e474f98b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ Changelog ========= +Releases for CakePHP 5 +------------- +* 12.0 + * Migrated to web-auth/webauthn-lib:^4.4 + * Migrated to robthree/twofactorauth:^2.0 + * Removed deprecated U2F + * Migrated old UserShell into command classes + * Added documentation about commands + Releases for CakePHP 4.3 ------------- @@ -35,7 +44,7 @@ Releases for CakePHP 4 * Ukrainian (uk) by @yarkm13 * Docs improvements * Fix DebugKit permissions issues - + * 9.0.2 * Added a custom Unauthorized Handler * If logged user access unauthorized url he is redirected to referer url or '/' if no referer url diff --git a/Docs/Documentation/Extending-the-Plugin.md b/Docs/Documentation/Extending-the-Plugin.md index f3a185cce..b2e3a131b 100644 --- a/Docs/Documentation/Extending-the-Plugin.md +++ b/Docs/Documentation/Extending-the-Plugin.md @@ -136,7 +136,13 @@ class MyUsersController extends AppController if ($this->components()->has('Security')) { $this->Security->setConfig( 'unlockedActions', - ['login', 'u2fRegister', 'u2fRegisterFinish', 'u2fAuthenticate', 'u2fAuthenticateFinish'] + [ + 'login', + 'webauthn2faRegister', + 'webauthn2faRegisterOptions', + 'webauthn2faAuthenticate', + 'webauthn2faAuthenticateOptions', + ] ); } } diff --git a/Docs/Documentation/Migration/11.x-12.0.md b/Docs/Documentation/Migration/11.x-12.0.md index b21674944..fcc32534b 100644 --- a/Docs/Documentation/Migration/11.x-12.0.md +++ b/Docs/Documentation/Migration/11.x-12.0.md @@ -13,11 +13,12 @@ Requirements Overview -------- - Removed the deprecated config key `'Auth.authenticate.all.contain'` you should use `'Auth.Profile.contain'` instead. +- Removed deprecated U2F code. U2F is no longer supported by chrome, we suggest using Webauthn as a replacement. - UsersShell logic was migrated into commands classes. - Security component was removed from CakePHP core, the usages in the plugin were updated with FormProtection component, for more information about the component, go to https://book.cakephp.org/5/en/controllers/components/form-protection.html -- + Webauthn Two-Factor Authentication ---------------------------------- It's required the version 4.4 of web-auth/webauthn-lib to use webauthn diff --git a/Docs/Documentation/Yubico-U2F.md b/Docs/Documentation/Yubico-U2F.md deleted file mode 100644 index fa2205703..000000000 --- a/Docs/Documentation/Yubico-U2F.md +++ /dev/null @@ -1,36 +0,0 @@ -YubicoKey U2F -============= - -**U2F is no longer supported by chrome, we suggest using Webauthn as a replacement** - -Enabling --------- - -First install yubico/u2flib-server using composer: - -``` -composer require yubico/u2flib-server:^1.0 -``` - -Then add this in your config/users.php file: - -```php - 'U2f.enabled' => true, -``` - -Disabling ---------- -You can disable it by adding this in your config/users.php file: - -```php - 'U2f.enabled' => false, -``` - -How does it work ----------------- -When the user log-in, he is requested to insert and tap his registered yubico key, -if this is the first time he access he need to register the yubico key. - -Please check the yubico site for more information about U2F -https://developers.yubico.com/U2F/ - diff --git a/Docs/Home.md b/Docs/Home.md index a178c1f0f..c3210d715 100644 --- a/Docs/Home.md +++ b/Docs/Home.md @@ -19,8 +19,7 @@ Documentation * [Intercept Login Action](Documentation/InterceptLoginAction.md) * [Social Authentication](Documentation/SocialAuthentication.md) * [Google Authenticator](Documentation/Two-Factor-Authenticator.md) -* [Webauthn Two-Factor Authentication](Documentation/WebauthnTwoFactorAuthenticator.md) -* [Yubico U2F](Documentation/Yubico-U2F.md) +* [Webauthn Two-Factor Authentication (Yubico Key compatible)](Documentation/WebauthnTwoFactorAuthenticator.md) * [UserHelper](Documentation/UserHelper.md) * [AuthLinkHelper](Documentation/AuthLinkHelper.md) * [Events](Documentation/Events.md) @@ -101,7 +100,6 @@ I want to * [social login](./Documentation/SocialAuthentication.md#setup) * [OTP Two-factor authenticator](./Documentation/Two-Factor-Authenticator.md) * [Webauthn Two-Factor Authentication](Documentation/WebauthnTwoFactorAuthenticator.md) - * [Yubico Key U2F Two-factor authenticator](./Documentation/Yubico-U2F.md) *
Authentication component diff --git a/README.md b/README.md index 040cdcbd7..8c1e7a633 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,8 @@ The **Users** plugin covers the following features: * Remember me (Cookie) via https://github.com/CakeDC/auth * Manage user's profile * Admin management -* Yubico U2F for Two-Factor Authentication * One-Time Password for Two-Factor Authentication -* Webauthn for Two-Factor Authentication +* Webauthn for Two-Factor Authentication (Yubico Key compatible) The plugin is here to provide users related features following 2 approaches: diff --git a/composer.json b/composer.json index fae7c5c78..84a91ff43 100644 --- a/composer.json +++ b/composer.json @@ -44,7 +44,6 @@ "luchianenco/oauth2-amazon": "^1.1", "google/recaptcha": "@stable", "robthree/twofactorauth": "^2.0", - "yubico/u2flib-server": "^1.0", "league/oauth1-client": "^1.7", "cakephp/cakephp-codesniffer": "^5.0", "web-auth/webauthn-lib": "^4.4", diff --git a/config/permissions.php b/config/permissions.php index 5c52467fb..e9cda4dce 100644 --- a/config/permissions.php +++ b/config/permissions.php @@ -73,12 +73,7 @@ // UserValidationTrait used in PasswordManagementTrait 'resendTokenValidation', 'linkSocial', - //U2F actions - 'u2f', - 'u2fRegister', - 'u2fRegisterFinish', - 'u2fAuthenticate', - 'u2fAuthenticateFinish', + //Webauthn2fa actions 'webauthn2fa', 'webauthn2faRegister', 'webauthn2faRegisterOptions', diff --git a/config/users.php b/config/users.php index b904fe4e7..3b4e93d81 100644 --- a/config/users.php +++ b/config/users.php @@ -136,10 +136,6 @@ // Random Number Generator provider (more on this later) 'rngprovider' => null, ], - 'U2f' => [ - 'enabled' => false, - 'checker' => \CakeDC\Auth\Authentication\DefaultU2fAuthenticationChecker::class, - ], 'Webauthn2fa' => [ 'enabled' => false, 'appName' => null,//App must set a valid name here diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index ae869cf3f..6fa398443 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -20,11 +20,6 @@ parameters: count: 3 path: src/Controller/UsersController.php - - - message: "#^Access to an undefined property Cake\\\\Datasource\\\\EntityInterface\\:\\:\\$u2f_registration\\.$#" - count: 1 - path: src/Controller/UsersController.php - - message: "#^Call to an undefined method Cake\\\\Controller\\\\Component\\:\\:handleLogin\\(\\)\\.$#" count: 3 diff --git a/src/Controller/Traits/U2fTrait.php b/src/Controller/Traits/U2fTrait.php deleted file mode 100644 index f0bc41a22..000000000 --- a/src/Controller/Traits/U2fTrait.php +++ /dev/null @@ -1,242 +0,0 @@ -getRequest()->getQueryParams(); - if (empty($url['?'])) { - unset($url['?']); - } - - return $this->redirect($url); - } - - /** - * U2f entry point - * - * @return \Cake\Http\Response|null - */ - public function u2f() - { - trigger_error(Plugin::DEPRECATED_MESSAGE_U2F, E_USER_DEPRECATED); - $data = $this->getU2fData(); - if (!$data['valid']) { - return $this->redirectWithQuery([ - 'action' => 'login', - ]); - } - if (!$data['registration']) { - return $this->redirectWithQuery([ - 'action' => 'u2fRegister', - ]); - } - - return $this->redirectWithQuery([ - 'action' => 'u2fAuthenticate', - ]); - } - - /** - * Show u2f register start step - * - * @return \Cake\Http\Response|null - * @throws \u2flib_server\Error - */ - public function u2fRegister() - { - trigger_error(Plugin::DEPRECATED_MESSAGE_U2F, E_USER_DEPRECATED); - $data = $this->getU2fData(); - if (!$data['valid']) { - return $this->redirectWithQuery([ - 'action' => 'login', - ]); - } - - if (!$data['registration']) { - [$registerRequest, $signs] = $this->createU2fLib()->getRegisterData(); - $this->getRequest()->getSession()->write('U2f.registerRequest', json_encode($registerRequest)); - $this->set(['registerRequest' => $registerRequest, 'signs' => $signs]); - - return null; - } - - return $this->redirectWithQuery([ - 'action' => 'u2fAuthenticate', - ]); - } - - /** - * Show u2f register finish step - * - * @return \Cake\Http\Response|null - */ - public function u2fRegisterFinish() - { - trigger_error(Plugin::DEPRECATED_MESSAGE_U2F, E_USER_DEPRECATED); - $data = $this->getU2fData(); - $request = json_decode($this->getRequest()->getSession()->read('U2f.registerRequest')); - $response = json_decode($this->getRequest()->getData('registerResponse')); - try { - $result = $this->createU2fLib()->doRegister($request, $response); - $additionalData = $data['user']->additional_data; - $additionalData['u2f_registration'] = $result; - $data['user']->additional_data = $additionalData; - $this->getUsersTable()->saveOrFail($data['user'], ['checkRules' => false]); - $this->getRequest()->getSession()->delete('U2f.registerRequest'); - - return $this->redirectWithQuery([ - 'action' => 'u2fAuthenticate', - ]); - } catch (\Exception $e) { - $this->getRequest()->getSession()->delete('U2f.registerRequest'); - - return $this->redirectWithQuery([ - 'action' => 'u2fRegister', - ]); - } - } - - /** - * Show u2f authenticate start step - * - * @return \Cake\Http\Response|null - */ - public function u2fAuthenticate() - { - trigger_error(Plugin::DEPRECATED_MESSAGE_U2F, E_USER_DEPRECATED); - $data = $this->getU2fData(); - if (!$data['valid']) { - return $this->redirectWithQuery([ - 'action' => 'login', - ]); - } - - if (!$data['registration']) { - return $this->redirectWithQuery([ - 'action' => 'u2fRegister', - ]); - } - $authenticateRequest = $this->createU2fLib()->getAuthenticateData([$data['registration']]); - $this->getRequest()->getSession()->write('U2f.authenticateRequest', json_encode($authenticateRequest)); - $this->set(['authenticateRequest' => $authenticateRequest]); - - return null; - } - - /** - * Show u2f Authenticate finish step - * - * @return \Cake\Http\Response|null - */ - public function u2fAuthenticateFinish() - { - trigger_error(Plugin::DEPRECATED_MESSAGE_U2F, E_USER_DEPRECATED); - $data = $this->getU2fData(); - $request = json_decode($this->getRequest()->getSession()->read('U2f.authenticateRequest')); - $response = json_decode($this->getRequest()->getData('authenticateResponse')); - - try { - $registration = $data['registration']; - $result = $this->createU2fLib()->doAuthenticate($request, [$registration], $response); - $registration->counter = $result->counter; - $additionalData = $data['user']->additional_data; - $additionalData['u2f_registration'] = $result; - $data['user']->additional_data = $additionalData; - $this->getUsersTable()->saveOrFail($data['user'], ['checkRules' => false]); - $this->getRequest()->getSession()->delete('U2f'); - $this->request->getSession()->delete(AuthenticationService::U2F_SESSION_KEY); - $this->request->getSession()->write(TwoFactorAuthenticator::USER_SESSION_KEY, $data['user']); - - return $this->redirectWithQuery(Configure::read('Auth.AuthenticationComponent.loginAction')); - } catch (\Exception $e) { - $this->getRequest()->getSession()->delete('U2f.authenticateRequest'); - - return $this->redirectWithQuery([ - 'action' => 'u2fAuthenticate', - ]); - } - } - - /** - * Create a u2f lib - * - * @return \u2flib_server\U2F - * @throws \u2flib_server\Error - */ - protected function createU2fLib() - { - $appId = $this->getRequest()->scheme() . '://' . $this->getRequest()->host(); - - return new U2F($appId); - } - - /** - * Get essential U2f data - * - * @return array - */ - protected function getU2fData() - { - $data = [ - 'valid' => false, - 'user' => null, - 'registration' => null, - ]; - $user = $this->getRequest()->getSession()->read(AuthenticationService::U2F_SESSION_KEY); - if (!isset($user['id'])) { - return $data; - } - if (!$this->request->is('ssl')) { - throw new \UnexpectedValueException(__d('cake_d_c/users', 'U2F requires SSL.')); - } - $entity = $this->getUsersTable()->get($user['id']); - $data['user'] = $user; - $data['valid'] = $this->getU2fAuthenticationChecker()->isEnabled(); - $data['registration'] = $entity->u2f_registration; - - return $data; - } - - /** - * Get the configured u2f authentication checker - * - * @return \CakeDC\Auth\Authentication\U2fAuthenticationCheckerInterface - */ - protected function getU2fAuthenticationChecker() - { - return (new U2fAuthenticationCheckerFactory())->build(); - } -} diff --git a/src/Controller/UsersController.php b/src/Controller/UsersController.php index 0a80a7d31..d271ddcd4 100644 --- a/src/Controller/UsersController.php +++ b/src/Controller/UsersController.php @@ -21,7 +21,6 @@ use CakeDC\Users\Controller\Traits\RegisterTrait; use CakeDC\Users\Controller\Traits\SimpleCrudTrait; use CakeDC\Users\Controller\Traits\SocialTrait; -use CakeDC\Users\Controller\Traits\U2fTrait; use CakeDC\Users\Controller\Traits\Webauthn2faTrait; /** @@ -40,7 +39,6 @@ class UsersController extends AppController use RegisterTrait; use SimpleCrudTrait; use SocialTrait; - use U2fTrait; use Webauthn2faTrait; /** @@ -56,10 +54,6 @@ public function initialize(): void 'unlockedActions', [ 'login', - 'u2fRegister', - 'u2fRegisterFinish', - 'u2fAuthenticate', - 'u2fAuthenticateFinish', 'webauthn2faRegister', 'webauthn2faRegisterOptions', 'webauthn2faAuthenticate', diff --git a/src/Loader/AuthenticationServiceLoader.php b/src/Loader/AuthenticationServiceLoader.php index de1b01b33..fd140c02a 100644 --- a/src/Loader/AuthenticationServiceLoader.php +++ b/src/Loader/AuthenticationServiceLoader.php @@ -15,7 +15,6 @@ use Cake\Core\Configure; use CakeDC\Auth\Authentication\AuthenticationService; -use CakeDC\Users\Plugin; use Psr\Http\Message\ServerRequestInterface; /** @@ -83,14 +82,9 @@ protected function loadAuthenticators($service) */ protected function loadTwoFactorAuthenticator($service) { - $u2fEnabled = Configure::read('U2f.enabled') !== false; - if ($u2fEnabled) { - trigger_error(Plugin::DEPRECATED_MESSAGE_U2F, E_USER_DEPRECATED); - } if ( Configure::read('OneTimePasswordAuthenticator.login') !== false || Configure::read('Webauthn2fa.enabled') !== false - || $u2fEnabled ) { $service->loadAuthenticator('CakeDC/Auth.TwoFactor', [ 'skipTwoFactorVerify' => true, diff --git a/src/Loader/MiddlewareQueueLoader.php b/src/Loader/MiddlewareQueueLoader.php index 240cd094c..bc3f45263 100644 --- a/src/Loader/MiddlewareQueueLoader.php +++ b/src/Loader/MiddlewareQueueLoader.php @@ -23,7 +23,6 @@ use CakeDC\Auth\Middleware\TwoFactorMiddleware; use CakeDC\Users\Middleware\SocialAuthMiddleware; use CakeDC\Users\Middleware\SocialEmailMiddleware; -use CakeDC\Users\Plugin; /** * Class MiddlewareQueueLoader @@ -96,15 +95,9 @@ protected function loadAuthenticationMiddleware( */ protected function load2faMiddleware(MiddlewareQueue $middlewareQueue) { - $u2fEnabled = Configure::read('U2f.enabled') !== false; - if ($u2fEnabled) { - trigger_error(Plugin::DEPRECATED_MESSAGE_U2F, E_USER_DEPRECATED); - } - if ( Configure::read('OneTimePasswordAuthenticator.login') !== false || Configure::read('Webauthn2fa.enabled') !== false - || $u2fEnabled ) { $middlewareQueue->add(TwoFactorMiddleware::class); } diff --git a/src/Model/Entity/User.php b/src/Model/Entity/User.php index 59cce931c..d286c2632 100644 --- a/src/Model/Entity/User.php +++ b/src/Model/Entity/User.php @@ -160,24 +160,6 @@ protected function _getAvatar() return $avatar; } - /** - * Return the u2f_registration inside additional_data - * - * @return object|null - */ - 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 $object->keyHandle !== null ? $object : null; - } - /** * Generate token_expires and token in a user * diff --git a/src/Utility/UsersUrl.php b/src/Utility/UsersUrl.php index f4c2f9458..737fb6139 100644 --- a/src/Utility/UsersUrl.php +++ b/src/Utility/UsersUrl.php @@ -123,7 +123,6 @@ private static function getDefaultConfigUrls() return [ '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, diff --git a/templates/Users/u2f_authenticate.php b/templates/Users/u2f_authenticate.php deleted file mode 100644 index 16ce1bf01..000000000 --- a/templates/Users/u2f_authenticate.php +++ /dev/null @@ -1,59 +0,0 @@ -Html->script('CakeDC/Users.u2f-api.js', ['block' => true]); -?> -
-
-
-
- Form->create(null, [ - 'url' => [ - 'action' => 'u2fAuthenticateFinish', - '?' => $this->request->getQueryParams() - ], - 'id' => 'u2fAuthenticateFrm' - ]) ?> - - Flash->render('auth') ?> - Flash->render() ?> -
-

-

-

-

-

Html->link( - __d('cake_d_c/users', 'Reload'), - ['action' => 'u2fAuthenticate'], - ['class' => 'btn btn-primary'] - )?>

-
- Form->hidden('authenticateResponse', ['secure' => false, 'id' => 'authenticateResponse'])?> - Form->end() ?> -
-
-
-
-Html->scriptStart(['block' => true]); -?> - setTimeout(function() { - var req = ; - var appId = req[0].appId; - var challenge = req[0].challenge; - - u2f.sign(appId, challenge, req, function(data) { - var targetForm = document.getElementById('u2fAuthenticateFrm'); - var targetInput = document.getElementById('authenticateResponse'); - if(data.errorCode && data.errorCode != 0) { - alert(""); - - return; - } - targetInput.value = JSON.stringify(data); - targetForm.submit(); - }); - }, 1000); -Html->scriptEnd();?> diff --git a/templates/Users/u2f_register.php b/templates/Users/u2f_register.php deleted file mode 100644 index 0ea8da5af..000000000 --- a/templates/Users/u2f_register.php +++ /dev/null @@ -1,64 +0,0 @@ -Html->script('CakeDC/Users.u2f-api.js', ['block' => true]); -?> -
-
-
-
- Form->create(null, [ - 'url' => [ - 'action' => 'u2fRegisterFinish', - '?' => $this->request->getQueryParams() - ], - 'id' => 'u2fRegisterFrm' - ]) ?> - - Flash->render('auth') ?> - Flash->render() ?> -
-

-

-

In order to enable your YubiKey the first step is to perform a registration.

-

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.

-

Html->link( - __d('cake_d_c/users', 'Reload'), - ['action' => 'u2fRegister'], - ['class' => 'btn btn-primary'] - )?>

-
- Form->hidden('registerResponse', ['secure' => false, 'id' => 'registerResponse'])?> - Form->end() ?> -
-
-
-
- $registerRequest->appId, - 'version' => $registerRequest->version, - 'challenge' => $registerRequest->challenge, - 'attestation' => 'direct' -]); -$this->Html->scriptStart(['block' => true]); -?> -setTimeout(function() { - var req = ; - var appId = req.appId; - var registerRequests = [req]; - u2f.register(appId, registerRequests, [], function(data) { - var targetForm = document.getElementById('u2fRegisterFrm'); - var targetInput = document.getElementById('registerResponse'); - - if(data.errorCode && data.errorCode != 0) { - alert(""); - - return; - } - targetInput.value = JSON.stringify(data); - targetForm.submit(); - }); -}, 1000); -Html->scriptEnd();?> diff --git a/tests/Fixture/UsersFixture.php b/tests/Fixture/UsersFixture.php index f5ff54cf6..6c0630138 100644 --- a/tests/Fixture/UsersFixture.php +++ b/tests/Fixture/UsersFixture.php @@ -48,12 +48,6 @@ public function init(): void 'created' => '2015-06-24 17:33:54', 'modified' => '2015-06-24 17:33:54', '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', diff --git a/tests/TestCase/Controller/Traits/Integration/LoginTraitIntegrationTest.php b/tests/TestCase/Controller/Traits/Integration/LoginTraitIntegrationTest.php index 7f9696ce8..cb5e71099 100644 --- a/tests/TestCase/Controller/Traits/Integration/LoginTraitIntegrationTest.php +++ b/tests/TestCase/Controller/Traits/Integration/LoginTraitIntegrationTest.php @@ -205,17 +205,17 @@ public function testLoginPostRequestRightPasswordIsEnabledOTP() * * @return void */ - public function testLoginPostRequestRightPasswordIsEnabledU2f() + public function testLoginPostRequestRightPasswordIsEnabledWebauthn2fa() { EventManager::instance()->on('TestApp.afterPluginBootstrap', function () { - Configure::write(['U2f.enabled' => true]); + Configure::write(['Webauthn2fa.enabled' => true, 'Webauthn2fa.appName' => 'TestUsers']); }); $this->enableRetainFlashMessages(); $this->post('/login', [ 'username' => 'user-2', 'password' => '12345', ]); - $this->assertRedirectContains('/users/u2f'); + $this->assertRedirectContains('/users/webauthn2fa'); } /** diff --git a/tests/TestCase/Controller/Traits/U2fTraitTest.php b/tests/TestCase/Controller/Traits/U2fTraitTest.php deleted file mode 100644 index 6d3c71132..000000000 --- a/tests/TestCase/Controller/Traits/U2fTraitTest.php +++ /dev/null @@ -1,771 +0,0 @@ -traitClassName = 'CakeDC\Users\Controller\UsersController'; - $this->traitMockMethods = ['dispatchEvent', 'isStopped', 'redirect', 'getUsersTable', 'set', 'createU2fLib', 'getData', 'getU2fAuthenticationChecker']; - - parent::setUp(); - - $this->Trait->expects($this->any()) - ->method('getU2fAuthenticationChecker') - ->willReturn(new DefaultU2fAuthenticationChecker()); - - $request = new ServerRequest(); - $this->Trait->setRequest($request); - Configure::write('U2f.enabled', true); - } - - /** - * 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; - } - - /** - * Data provider for testU2User - * - * @return array - */ - public function dataProviderU2User() - { - $empty = []; - $withRegistration = new User([ - 'id' => '00000000-0000-0000-0000-000000000001', - 'username' => 'user-1', - ]); - $withoutRegistration = new User([ - 'id' => '00000000-0000-0000-0000-000000000002', - 'username' => 'user-2', - ]); - - return [ - // [$empty, ['action' => 'login']], - // [$withoutRegistration, ['action' => 'u2fRegister']], - [$withRegistration, ['action' => 'u2fAuthenticate']], - ]; - } - - /** - * Test u2f method - * - * @param array $userData session user data - * @param mixed $redirect expetected redirect - * @dataProvider dataProviderU2User - * @return void - */ - public function testU2fCustomUser($userData, $redirect) - { - $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)); - $response = new Response([ - 'body' => (string)time(), - ]); - $this->Trait->expects($this->once()) - ->method('redirect') - ->with( - $this->equalTo($redirect) - )->will($this->returnValue($response)); - $this->_mockSession([ - 'U2f.User' => $userData, - ]); - $actual = $this->Trait->u2f(); - $this->assertSame($response, $actual); - } - - /** - * Test u2fRegister method - * - * @return void - */ - public function testU2fRegisterOkay() - { - $request = $this->getMockBuilder('Cake\Http\ServerRequest') - ->setMethods(['getSession', 'is']) - ->getMock(); - $this->Trait->setRequest($request); - $request->expects($this->once()) - ->method('is') - ->with( - $this->equalTo('ssl') - )->will($this->returnValue(true)); - - $u2fLib = $this->getMockBuilder(U2F::class) - ->setConstructorArgs(['https://localhost']) - ->setMethods(['getRegisterData']) - ->getMock(); - - $registerRequest = new RegisterRequest('sample chalange', 'https://localhost'); - $signs = [ - ['fake' => new \stdClass()], - ['fake2' => new \stdClass()], - ]; - $u2fLib->expects($this->once()) - ->method('getRegisterData') - ->will($this->returnValue([$registerRequest, $signs])); - - $this->Trait->expects($this->once()) - ->method('createU2fLib') - ->will($this->returnValue($u2fLib)); - $this->Trait->expects($this->once()) - ->method('set') - ->with( - $this->equalTo([ - 'registerRequest' => $registerRequest, - 'signs' => $signs, - ]) - ); - $this->Trait->expects($this->never()) - ->method('redirect'); - - $this->_mockSession([ - 'U2f.User' => new User([ - 'id' => '00000000-0000-0000-0000-000000000002', - 'username' => 'user-2', - ]), - ]); - $actual = $this->Trait->u2fRegister(); - $this->assertNull($actual); - $actual = $this->Trait->getRequest()->getSession()->read('U2f.registerRequest'); - $expected = json_encode($registerRequest); - $this->assertEquals($expected, $actual); - } - - /** - * Data provider for testU2fRegisterRedirect - * - * @return array - */ - public function dataProviderU2fRegisterRedirect() - { - $empty = []; - $withRegistration = new User([ - 'id' => '00000000-0000-0000-0000-000000000001', - 'username' => 'user-1', - ]); - - return [ - [$empty, ['action' => 'login']], - [$withRegistration, ['action' => 'u2fAuthenticate']], - ]; - } - - /** - * Test u2fRegister method - * - * @param array $userData session user data - * @param mixed $redirect expetected redirect - * @dataProvider dataProviderU2fRegisterRedirect - * @return void - */ - public function testU2fRegisterRedirect($userData, $redirect) - { - $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)); - $this->Trait->expects($this->never()) - ->method('createU2fLib'); - - $this->Trait->expects($this->never()) - ->method('set'); - - $this->_mockSession([ - 'U2f.User' => $userData, - ]); - $response = new Response([ - 'body' => (string)time(), - ]); - $this->Trait->expects($this->once()) - ->method('redirect') - ->with( - $this->equalTo($redirect) - )->will($this->returnValue($response)); - - $actual = $this->Trait->u2fRegister(); - $this->assertSame($response, $actual); - $actual = $this->Trait->getRequest()->getSession()->read('U2f.registerRequest'); - $expected = null; - $this->assertEquals($expected, $actual); - } - - /** - * Test u2fRegister method - * - * @return void - */ - public function testU2fRegisterFinishOkay() - { - $request = $this->getMockBuilder('Cake\Http\ServerRequest') - ->setMethods(['getSession', 'is', 'getData']) - ->getMock(); - $this->Trait->setRequest($request); - $request->expects($this->once()) - ->method('is') - ->with( - $this->equalTo('ssl') - )->will($this->returnValue(true)); - - $u2fLib = $this->getMockBuilder(U2F::class) - ->setConstructorArgs(['https://localhost']) - ->setMethods(['doRegister']) - ->getMock(); - - $registerRequest = new RegisterRequest('sample chalange', 'https://localhost'); - $registerRequest = json_decode(json_encode($registerRequest)); - $signs = [ - ['fake' => new \stdClass()], - ['fake2' => new \stdClass()], - ]; - $registerResponse = json_decode(json_encode([ - 'fakeA' => 'fakevaluea', - 'fakeB' => 'fakevalueb', - ])); - $registration = new Registration(); - $registration->certificate = 'user registration cert ' . time(); - $registration->counter = 1; - $registration->publicKey = 'pub skska08u90234230990'; - $registration->keyHandle = 'hahdofa02390423udu9ma0dumfá0dsufm2um9432uu903u923'; - - $this->Trait->getRequest()->expects($this->once()) - ->method('getData') - ->with($this->equalTo('registerResponse')) - ->will($this->returnValue(json_encode($registerResponse))); - $this->_mockSession([ - 'U2f' => [ - 'User' => new User([ - 'id' => '00000000-0000-0000-0000-000000000002', - 'username' => 'user-2', - ]), - 'registerRequest' => json_encode($registerRequest), - ], - ]); - $u2fLib->expects($this->once()) - ->method('doRegister') - ->with( - $this->equalTo($registerRequest), - $this->equalTo($registerResponse) - ) - ->will($this->returnValue($registration)); - - $this->Trait->expects($this->once()) - ->method('createU2fLib') - ->will($this->returnValue($u2fLib)); - - $actual = $this->Trait->getRequest()->getSession()->read('U2f'); - $this->assertNotNull($actual); - - $response = new Response(); - $this->Trait->expects($this->once()) - ->method('redirect') - ->with( - $this->equalTo([ - 'action' => 'u2fAuthenticate', - ]) - )->will($this->returnValue($response)); - - $actual = $this->Trait->u2fRegisterFinish(); - $this->assertSame($response, $actual); - $actual = $this->Trait->getRequest()->getSession()->read('U2f'); - $this->assertEquals('00000000-0000-0000-0000-000000000002', $actual['User']['id']); - $this->assertEquals('user-2', $actual['User']['username']); - $this->assertNotEmpty($actual['User']['additional_data']); - $this->assertNotEmpty($actual['User']['additional_data']['u2f_registration']); - - $saveUser = TableRegistry::getTableLocator() - ->get('CakeDC/Users.Users') - ->get('00000000-0000-0000-0000-000000000002'); - - $savedRegistration = $saveUser->u2f_registration; - $this->assertNotNull($savedRegistration); - $this->assertEquals(json_encode($registration), json_encode($savedRegistration)); - - $registration = new Registration(); - $registration->certificate = 'user registration cert ' . time(); - $registration->counter = 1; - $registration->publicKey = 'pub skska08u90234230990'; - $registration->keyHandle = 'hahdofa02390423udu9ma0dumfá0dsufm2um9432uu903u923'; - } - - /** - * Test u2fRegister method - * - * @return void - */ - public function testU2fRegisterFinishException() - { - $request = $this->getMockBuilder('Cake\Http\ServerRequest') - ->setMethods(['getSession', 'is', 'getData']) - ->getMock(); - $this->Trait->setRequest($request); - $request->expects($this->once()) - ->method('is') - ->with( - $this->equalTo('ssl') - )->will($this->returnValue(true)); - - $u2fLib = $this->getMockBuilder(U2F::class) - ->setConstructorArgs(['https://localhost']) - ->setMethods(['doRegister']) - ->getMock(); - - $registerRequest = new RegisterRequest('sample chalange', 'https://localhost'); - $registerRequest = json_decode(json_encode($registerRequest)); - $registerResponse = json_decode(json_encode([ - 'fakeA' => 'fakevaluea', - 'fakeB' => 'fakevalueb', - ])); - $registration = new Registration(); - $registration->certificate = 'user registration cert ' . time(); - $registration->counter = 1; - $registration->publicKey = 'pub skska08u90234230990'; - $registration->keyHandle = 'hahdofa02390423udu9ma0dumfá0dsufm2um9432uu903u923'; - - $this->Trait->getRequest()->expects($this->once()) - ->method('getData') - ->with($this->equalTo('registerResponse')) - ->will($this->returnValue(json_encode($registerResponse))); - $this->_mockSession([ - 'U2f' => [ - 'User' => new User([ - 'id' => '00000000-0000-0000-0000-000000000002', - 'username' => 'user-2', - ]), - 'registerRequest' => json_encode($registerRequest), - ], - ]); - $u2fLib->expects($this->once()) - ->method('doRegister') - ->with( - $this->equalTo($registerRequest), - $this->equalTo($registerResponse) - ) - ->will($this->throwException(new \Exception('Invalid request'))); - - $this->Trait->expects($this->once()) - ->method('createU2fLib') - ->will($this->returnValue($u2fLib)); - - $actual = $this->Trait->getRequest()->getSession()->read('U2f'); - $this->assertNotNull($actual); - - $response = new Response(); - $this->Trait->expects($this->once()) - ->method('redirect') - ->with( - $this->equalTo([ - 'action' => 'u2fRegister', - ]) - )->will($this->returnValue($response)); - - $actual = $this->Trait->u2fRegisterFinish(); - $this->assertSame($response, $actual); - $actual = $this->Trait->getRequest()->getSession()->read('U2f'); - $this->assertEquals( - [ - 'User' => new User([ - 'id' => '00000000-0000-0000-0000-000000000002', - 'username' => 'user-2', - ]), - ], - $actual - ); - - $saveUser = TableRegistry::getTableLocator() - ->get('CakeDC/Users.Users') - ->get('00000000-0000-0000-0000-000000000002'); - - $savedRegistration = $saveUser->u2f_registration; - $this->assertNull($savedRegistration); - - $registration = new Registration(); - $registration->certificate = 'user registration cert ' . time(); - $registration->counter = 1; - $registration->publicKey = 'pub skska08u90234230990'; - $registration->keyHandle = 'hahdofa02390423udu9ma0dumfá0dsufm2um9432uu903u923'; - } - - /** - * Data provider for testU2fAuthenticateRedirectCustomUser - * - * @return array - */ - public function dataProviderU2fAuthenticateRedirectCustomUser() - { - $empty = []; - $withWhoutRegistration = new User([ - 'id' => '00000000-0000-0000-0000-000000000002', - 'username' => 'user-2', - ]); - - return [ - [$empty, ['action' => 'login']], - [$withWhoutRegistration, ['action' => 'u2fRegister']], - ]; - } - - /** - * Test u2fAuthenticate method redirect cases - * - * @param array $userData session user data - * @param mixed $redirect expetected redirect - * @dataProvider dataProviderU2fAuthenticateRedirectCustomUser - * @return void - */ - public function testU2fAuthenticateRedirectCustomUser($userData, $redirect) - { - $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)); - $response = new Response([ - 'body' => (string)time(), - ]); - $this->Trait->expects($this->once()) - ->method('redirect') - ->with( - $this->equalTo($redirect) - )->will($this->returnValue($response)); - $this->_mockSession([ - 'U2f.User' => $userData, - ]); - $actual = $this->Trait->u2fAuthenticate(); - $this->assertSame($response, $actual); - } - - /** - * Test u2fAuthenticate method - * - * @return void - */ - public function testU2fAuthenticate() - { - $request = $this->getMockBuilder('Cake\Http\ServerRequest') - ->setMethods(['getSession', 'is']) - ->getMock(); - $this->Trait->setRequest($request); - $request->expects($this->once()) - ->method('is') - ->with( - $this->equalTo('ssl') - )->will($this->returnValue(true)); - - $u2fLib = $this->getMockBuilder(U2F::class) - ->setConstructorArgs(['https://localhost']) - ->setMethods(['getAuthenticateData']) - ->getMock(); - - $signs = [ - ['fake' => new \stdClass()], - ['fake2' => new \stdClass()], - ]; - $reg1 = [ - 'keyHandle' => 'fake key handle', - 'publicKey' => 'afdoaj0-23u423-ad ujsf-as8-0-afsd', - 'certificate' => '23jdsfoasdj0f9sa082304823423', - 'counter' => 1, - ]; - $registrations = [ - (object)$reg1, - ]; - $u2fLib->expects($this->once()) - ->method('getAuthenticateData') - ->with( - $this->equalTo($registrations) - ) - ->will($this->returnValue($signs)); - - $this->Trait->expects($this->once()) - ->method('createU2fLib') - ->will($this->returnValue($u2fLib)); - $this->Trait->expects($this->once()) - ->method('set') - ->with( - $this->equalTo([ - 'authenticateRequest' => $signs, - ]) - ); - $this->Trait->expects($this->never()) - ->method('redirect'); - - $this->_mockSession([ - 'U2f.User' => new User([ - 'id' => '00000000-0000-0000-0000-000000000001', - 'username' => 'user-1', - ]), - ]); - $actual = $this->Trait->u2fAuthenticate(); - $this->assertNull($actual); - $actual = $this->Trait->getRequest()->getSession()->read('U2f.authenticateRequest'); - $expected = json_encode($signs); - $this->assertEquals($expected, $actual); - } - - /** - * Test u2fAuthenticateFinish method - * - * @return void - */ - public function testU2fAutheticateFinishOkay() - { - $user = TableRegistry::getTableLocator() - ->get('CakeDC/Users.Users') - ->get('00000000-0000-0000-0000-000000000001'); - $this->assertNotNull($user->u2f_registration); - - $registration = $user->u2f_registration; - $registrationEntityResult = new Registration(); - $registrationEntityResult->keyHandle = $registration->keyHandle; - $registrationEntityResult->publicKey = $registration->publicKey; - $registrationEntityResult->counter = $registration->counter + 1; - $registrationEntityResult->certificate = $registration->certificate; - - $request = $this->getMockBuilder('Cake\Http\ServerRequest') - ->setMethods(['getSession', 'is', 'getData']) - ->getMock(); - $this->Trait->setRequest($request); - $request->expects($this->once()) - ->method('is') - ->with( - $this->equalTo('ssl') - )->will($this->returnValue(true)); - $u2fLib = $this->getMockBuilder(U2F::class) - ->setConstructorArgs(['https://localhost']) - ->setMethods(['doAuthenticate']) - ->getMock(); - - $signs = json_decode(json_encode([ - ['fake' => new \stdClass()], - ['fake2' => new \stdClass()], - ])); - $authenticateResponse = json_decode(json_encode([ - 'fakeA' => 'fakevaluea', - 'fakeB' => 'fakevalueb', - ])); - - $this->Trait->getRequest()->expects($this->once()) - ->method('getData') - ->with($this->equalTo('authenticateResponse')) - ->will($this->returnValue(json_encode($authenticateResponse))); - $this->_mockSession([ - 'U2f' => [ - 'User' => new User([ - 'id' => '00000000-0000-0000-0000-000000000001', - 'username' => 'user-1', - ]), - 'authenticateRequest' => json_encode($signs), - ], - ]); - - $u2fLib->expects($this->once()) - ->method('doAuthenticate') - ->with( - $this->equalTo($signs), - $this->equalTo([$registration]), - $this->equalTo($authenticateResponse) - ) - ->will($this->returnValue($registrationEntityResult)); - - $this->Trait->expects($this->once()) - ->method('createU2fLib') - ->will($this->returnValue($u2fLib)); - - $actual = $this->Trait->getRequest()->getSession()->read('U2f'); - $this->assertNotNull($actual); - - $response = new Response(); - $this->Trait->expects($this->once()) - ->method('redirect') - ->with([ - 'plugin' => 'CakeDC/Users', - 'controller' => 'Users', - 'action' => 'login', - 'prefix' => false, - ])->will($this->returnValue($response)); - - $actual = $this->Trait->u2fAuthenticateFinish(); - $this->assertSame($response, $actual); - $actual = $this->Trait->getRequest()->getSession()->read('U2f'); - $this->assertNull($actual); - - $updatedEntity = TableRegistry::getTableLocator() - ->get('CakeDC/Users.Users') - ->get($user['id']) - ->u2f_registration; - - $this->assertEquals($registrationEntityResult->counter, $updatedEntity->counter); - } - - /** - * Test u2fAuthenticateFinish method with exception - * - * @return void - */ - public function testU2fAutheticateFinishWithException() - { - $saveUser = TableRegistry::getTableLocator() - ->get('CakeDC/Users.Users') - ->get('00000000-0000-0000-0000-000000000001'); - - $savedRegistration = $saveUser->u2f_registration; - $this->assertNotNull($savedRegistration); - $registration = $saveUser->u2f_registration; - $counter = $registration->counter; - $this->assertNotNull($registration); - - $request = $this->getMockBuilder('Cake\Http\ServerRequest') - ->setMethods(['getSession', 'is', 'getData']) - ->getMock(); - $this->Trait->setRequest($request); - $request->expects($this->once()) - ->method('is') - ->with( - $this->equalTo('ssl') - )->will($this->returnValue(true)); - - $u2fLib = $this->getMockBuilder(U2F::class) - ->setConstructorArgs(['https://localhost']) - ->setMethods(['doAuthenticate']) - ->getMock(); - - $signs = json_decode(json_encode([ - ['fake' => new \stdClass()], - ['fake2' => new \stdClass()], - ])); - $authenticateResponse = json_decode(json_encode([ - 'fakeA' => 'fakevaluea', - 'fakeB' => 'fakevalueb', - ])); - - $this->Trait->getRequest()->expects($this->once()) - ->method('getData') - ->with($this->equalTo('authenticateResponse')) - ->will($this->returnValue(json_encode($authenticateResponse))); - - $this->_mockSession([ - 'U2f' => [ - 'User' => new User([ - 'id' => '00000000-0000-0000-0000-000000000001', - 'username' => 'user-1', - ]), - 'authenticateRequest' => json_encode($signs), - ], - ]); - - $u2fLib->expects($this->once()) - ->method('doAuthenticate') - ->with( - $this->equalTo($signs), - $this->equalTo([$registration]), - $this->equalTo($authenticateResponse) - ) - ->will($this->throwException(new \Exception('Invalid'))); - - $this->Trait->expects($this->once()) - ->method('createU2fLib') - ->will($this->returnValue($u2fLib)); - - $response = new Response(); - $this->Trait->expects($this->once()) - ->method('redirect') - ->with(['action' => 'u2fAuthenticate']) - ->will($this->returnValue($response)); - - $actual = $this->Trait->u2fAuthenticateFinish(); - $this->assertSame($response, $actual); - $actual = $this->Trait->getRequest()->getSession()->read('U2f'); - $this->assertEquals( - [ - 'User' => new User([ - 'id' => '00000000-0000-0000-0000-000000000001', - 'username' => 'user-1', - ]), - ], - $actual - ); - - $updatedEntityUser = TableRegistry::getTableLocator() - ->get('CakeDC/Users.Users') - ->get('00000000-0000-0000-0000-000000000001'); - - $updatedEntity = $updatedEntityUser->u2f_registration; - $this->assertEquals($counter, $updatedEntity->counter); - } -} diff --git a/tests/TestCase/Controller/Traits/Webauthn2faTraitTest.php b/tests/TestCase/Controller/Traits/Webauthn2faTraitTest.php index 1740438ca..ed2c57a93 100644 --- a/tests/TestCase/Controller/Traits/Webauthn2faTraitTest.php +++ b/tests/TestCase/Controller/Traits/Webauthn2faTraitTest.php @@ -333,7 +333,7 @@ public function testWebauthn2faRegisterError() ); $this->Trait->setRequest($request); $this->expectException(\Exception::class); - $this->expectErrorMessage('Testing error exception for webauthn2faRegister'); + $this->expectExceptionMessage('Testing error exception for webauthn2faRegister'); $this->Trait->webauthn2faRegister($adapter); } diff --git a/tests/TestCase/PluginTest.php b/tests/TestCase/PluginTest.php index d4fedf44f..776c06dca 100644 --- a/tests/TestCase/PluginTest.php +++ b/tests/TestCase/PluginTest.php @@ -231,12 +231,6 @@ public function dataProviderConfigUsersUrls() 'controller' => 'Users', 'action' => 'profile', ]; - $defaultU2fStartAction = [ - 'prefix' => false, - 'plugin' => 'CakeDC/Users', - 'controller' => 'Users', - 'action' => 'u2f', - ]; $defaultLoginAction = [ 'prefix' => false, 'plugin' => 'CakeDC/Users', @@ -253,7 +247,6 @@ public function dataProviderConfigUsersUrls() return [ ['Users.Profile.route', $defaultProfileAction], ['OneTimePasswordAuthenticator.verifyAction', $defaultVerifyAction], - ['U2f.startAction', $defaultU2fStartAction], ['Auth.AuthenticationComponent.loginAction', $defaultLoginAction], ['Auth.AuthenticationComponent.logoutRedirect', $defaultLoginAction], ['Auth.AuthenticationComponent.loginRedirect', '/'], diff --git a/tests/TestCase/Provider/AuthenticationServiceProviderTest.php b/tests/TestCase/Provider/AuthenticationServiceProviderTest.php index 7f9a73cfe..271374d9a 100644 --- a/tests/TestCase/Provider/AuthenticationServiceProviderTest.php +++ b/tests/TestCase/Provider/AuthenticationServiceProviderTest.php @@ -114,7 +114,7 @@ public function testGetAuthenticationService() ], ]; $actual = []; - foreach ($authenticators as $key => $value) { + foreach ($authenticators as $value) { $config = $value->getConfig(); $actual[get_class($value)] = $config; } diff --git a/webroot/js/u2f-api.js b/webroot/js/u2f-api.js deleted file mode 100644 index ac478feff..000000000 --- a/webroot/js/u2f-api.js +++ /dev/null @@ -1,748 +0,0 @@ -//Copyright 2014-2015 Google Inc. All rights reserved. - -//Use of this source code is governed by a BSD-style -//license that can be found in the LICENSE file or at -//https://developers.google.com/open-source/licenses/bsd - -/** - * @fileoverview The U2F api. - */ -'use strict'; - - -/** - * Namespace for the U2F api. - * @type {Object} - */ -var u2f = u2f || {}; - -/** - * FIDO U2F Javascript API Version - * @number - */ -var js_api_version; - -/** - * The U2F extension id - * @const {string} - */ -// The Chrome packaged app extension ID. -// Uncomment this if you want to deploy a server instance that uses -// the package Chrome app and does not require installing the U2F Chrome extension. - u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd'; -// The U2F Chrome extension ID. -// Uncomment this if you want to deploy a server instance that uses -// the U2F Chrome extension to authenticate. -// u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne'; - - -/** - * Message types for messsages to/from the extension - * @const - * @enum {string} - */ -u2f.MessageTypes = { - 'U2F_REGISTER_REQUEST': 'u2f_register_request', - 'U2F_REGISTER_RESPONSE': 'u2f_register_response', - 'U2F_SIGN_REQUEST': 'u2f_sign_request', - 'U2F_SIGN_RESPONSE': 'u2f_sign_response', - 'U2F_GET_API_VERSION_REQUEST': 'u2f_get_api_version_request', - 'U2F_GET_API_VERSION_RESPONSE': 'u2f_get_api_version_response' -}; - - -/** - * Response status codes - * @const - * @enum {number} - */ -u2f.ErrorCodes = { - 'OK': 0, - 'OTHER_ERROR': 1, - 'BAD_REQUEST': 2, - 'CONFIGURATION_UNSUPPORTED': 3, - 'DEVICE_INELIGIBLE': 4, - 'TIMEOUT': 5 -}; - - -/** - * A message for registration requests - * @typedef {{ - * type: u2f.MessageTypes, - * appId: ?string, - * timeoutSeconds: ?number, - * requestId: ?number - * }} - */ -u2f.U2fRequest; - - -/** - * A message for registration responses - * @typedef {{ - * type: u2f.MessageTypes, - * responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse), - * requestId: ?number - * }} - */ -u2f.U2fResponse; - - -/** - * An error object for responses - * @typedef {{ - * errorCode: u2f.ErrorCodes, - * errorMessage: ?string - * }} - */ -u2f.Error; - -/** - * Data object for a single sign request. - * @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC, USB_INTERNAL}} - */ -u2f.Transport; - - -/** - * Data object for a single sign request. - * @typedef {Array} - */ -u2f.Transports; - -/** - * Data object for a single sign request. - * @typedef {{ - * version: string, - * challenge: string, - * keyHandle: string, - * appId: string - * }} - */ -u2f.SignRequest; - - -/** - * Data object for a sign response. - * @typedef {{ - * keyHandle: string, - * signatureData: string, - * clientData: string - * }} - */ -u2f.SignResponse; - - -/** - * Data object for a registration request. - * @typedef {{ - * version: string, - * challenge: string - * }} - */ -u2f.RegisterRequest; - - -/** - * Data object for a registration response. - * @typedef {{ - * version: string, - * keyHandle: string, - * transports: Transports, - * appId: string - * }} - */ -u2f.RegisterResponse; - - -/** - * Data object for a registered key. - * @typedef {{ - * version: string, - * keyHandle: string, - * transports: ?Transports, - * appId: ?string - * }} - */ -u2f.RegisteredKey; - - -/** - * Data object for a get API register response. - * @typedef {{ - * js_api_version: number - * }} - */ -u2f.GetJsApiVersionResponse; - - -//Low level MessagePort API support - -/** - * Sets up a MessagePort to the U2F extension using the - * available mechanisms. - * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback - */ -u2f.getMessagePort = function(callback) { - if (typeof chrome != 'undefined' && chrome.runtime) { - // The actual message here does not matter, but we need to get a reply - // for the callback to run. Thus, send an empty signature request - // in order to get a failure response. - var msg = { - type: u2f.MessageTypes.U2F_SIGN_REQUEST, - signRequests: [] - }; - chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() { - if (!chrome.runtime.lastError) { - // We are on a whitelisted origin and can talk directly - // with the extension. - u2f.getChromeRuntimePort_(callback); - } else { - // chrome.runtime was available, but we couldn't message - // the extension directly, use iframe - u2f.getIframePort_(callback); - } - }); - } else if (u2f.isAndroidChrome_()) { - u2f.getAuthenticatorPort_(callback); - } else if (u2f.isIosChrome_()) { - u2f.getIosPort_(callback); - } else { - // chrome.runtime was not available at all, which is normal - // when this origin doesn't have access to any extensions. - u2f.getIframePort_(callback); - } -}; - -/** - * Detect chrome running on android based on the browser's useragent. - * @private - */ -u2f.isAndroidChrome_ = function() { - var userAgent = navigator.userAgent; - return userAgent.indexOf('Chrome') != -1 && - userAgent.indexOf('Android') != -1; -}; - -/** - * Detect chrome running on iOS based on the browser's platform. - * @private - */ -u2f.isIosChrome_ = function() { - return ["iPhone", "iPad", "iPod"].indexOf(navigator.platform) > -1; -}; - -/** - * Connects directly to the extension via chrome.runtime.connect. - * @param {function(u2f.WrappedChromeRuntimePort_)} callback - * @private - */ -u2f.getChromeRuntimePort_ = function(callback) { - var port = chrome.runtime.connect(u2f.EXTENSION_ID, - {'includeTlsChannelId': true}); - setTimeout(function() { - callback(new u2f.WrappedChromeRuntimePort_(port)); - }, 0); -}; - -/** - * Return a 'port' abstraction to the Authenticator app. - * @param {function(u2f.WrappedAuthenticatorPort_)} callback - * @private - */ -u2f.getAuthenticatorPort_ = function(callback) { - setTimeout(function() { - callback(new u2f.WrappedAuthenticatorPort_()); - }, 0); -}; - -/** - * Return a 'port' abstraction to the iOS client app. - * @param {function(u2f.WrappedIosPort_)} callback - * @private - */ -u2f.getIosPort_ = function(callback) { - setTimeout(function() { - callback(new u2f.WrappedIosPort_()); - }, 0); -}; - -/** - * A wrapper for chrome.runtime.Port that is compatible with MessagePort. - * @param {Port} port - * @constructor - * @private - */ -u2f.WrappedChromeRuntimePort_ = function(port) { - this.port_ = port; -}; - -/** - * Format and return a sign request compliant with the JS API version supported by the extension. - * @param {Array} signRequests - * @param {number} timeoutSeconds - * @param {number} reqId - * @return {Object} - */ -u2f.formatSignRequest_ = - function(appId, challenge, registeredKeys, timeoutSeconds, reqId) { - if (js_api_version === undefined || js_api_version < 1.1) { - // Adapt request to the 1.0 JS API - var signRequests = []; - for (var i = 0; i < registeredKeys.length; i++) { - signRequests[i] = { - version: registeredKeys[i].version, - challenge: challenge, - keyHandle: registeredKeys[i].keyHandle, - appId: appId - }; - } - return { - type: u2f.MessageTypes.U2F_SIGN_REQUEST, - signRequests: signRequests, - timeoutSeconds: timeoutSeconds, - requestId: reqId - }; - } - // JS 1.1 API - return { - type: u2f.MessageTypes.U2F_SIGN_REQUEST, - appId: appId, - challenge: challenge, - registeredKeys: registeredKeys, - timeoutSeconds: timeoutSeconds, - requestId: reqId - }; -}; - -/** - * Format and return a register request compliant with the JS API version supported by the extension.. - * @param {Array} signRequests - * @param {Array} signRequests - * @param {number} timeoutSeconds - * @param {number} reqId - * @return {Object} - */ -u2f.formatRegisterRequest_ = - function(appId, registeredKeys, registerRequests, timeoutSeconds, reqId) { - if (js_api_version === undefined || js_api_version < 1.1) { - // Adapt request to the 1.0 JS API - for (var i = 0; i < registerRequests.length; i++) { - registerRequests[i].appId = appId; - } - var signRequests = []; - for (var i = 0; i < registeredKeys.length; i++) { - signRequests[i] = { - version: registeredKeys[i].version, - challenge: registerRequests[0], - keyHandle: registeredKeys[i].keyHandle, - appId: appId - }; - } - return { - type: u2f.MessageTypes.U2F_REGISTER_REQUEST, - signRequests: signRequests, - registerRequests: registerRequests, - timeoutSeconds: timeoutSeconds, - requestId: reqId - }; - } - // JS 1.1 API - return { - type: u2f.MessageTypes.U2F_REGISTER_REQUEST, - appId: appId, - registerRequests: registerRequests, - registeredKeys: registeredKeys, - timeoutSeconds: timeoutSeconds, - requestId: reqId - }; -}; - - -/** - * Posts a message on the underlying channel. - * @param {Object} message - */ -u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) { - this.port_.postMessage(message); -}; - - -/** - * Emulates the HTML 5 addEventListener interface. Works only for the - * onmessage event, which is hooked up to the chrome.runtime.Port.onMessage. - * @param {string} eventName - * @param {function({data: Object})} handler - */ -u2f.WrappedChromeRuntimePort_.prototype.addEventListener = - function(eventName, handler) { - var name = eventName.toLowerCase(); - if (name == 'message' || name == 'onmessage') { - this.port_.onMessage.addListener(function(message) { - // Emulate a minimal MessageEvent object - handler({'data': message}); - }); - } else { - console.error('WrappedChromeRuntimePort only supports onMessage'); - } -}; - -/** - * Wrap the Authenticator app with a MessagePort interface. - * @constructor - * @private - */ -u2f.WrappedAuthenticatorPort_ = function() { - this.requestId_ = -1; - this.requestObject_ = null; -} - -/** - * Launch the Authenticator intent. - * @param {Object} message - */ -u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) { - var intentUrl = - u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ + - ';S.request=' + encodeURIComponent(JSON.stringify(message)) + - ';end'; - document.location = intentUrl; -}; - -/** - * Tells what type of port this is. - * @return {String} port type - */ -u2f.WrappedAuthenticatorPort_.prototype.getPortType = function() { - return "WrappedAuthenticatorPort_"; -}; - - -/** - * Emulates the HTML 5 addEventListener interface. - * @param {string} eventName - * @param {function({data: Object})} handler - */ -u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function(eventName, handler) { - var name = eventName.toLowerCase(); - if (name == 'message') { - var self = this; - /* Register a callback to that executes when - * chrome injects the response. */ - window.addEventListener( - 'message', self.onRequestUpdate_.bind(self, handler), false); - } else { - console.error('WrappedAuthenticatorPort only supports message'); - } -}; - -/** - * Callback invoked when a response is received from the Authenticator. - * @param function({data: Object}) callback - * @param {Object} message message Object - */ -u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ = - function(callback, message) { - var messageObject = JSON.parse(message.data); - var intentUrl = messageObject['intentURL']; - - var errorCode = messageObject['errorCode']; - var responseObject = null; - if (messageObject.hasOwnProperty('data')) { - responseObject = /** @type {Object} */ ( - JSON.parse(messageObject['data'])); - } - - callback({'data': responseObject}); -}; - -/** - * Base URL for intents to Authenticator. - * @const - * @private - */ -u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ = - 'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE'; - -/** - * Wrap the iOS client app with a MessagePort interface. - * @constructor - * @private - */ -u2f.WrappedIosPort_ = function() {}; - -/** - * Launch the iOS client app request - * @param {Object} message - */ -u2f.WrappedIosPort_.prototype.postMessage = function(message) { - var str = JSON.stringify(message); - var url = "u2f://auth?" + encodeURI(str); - location.replace(url); -}; - -/** - * Tells what type of port this is. - * @return {String} port type - */ -u2f.WrappedIosPort_.prototype.getPortType = function() { - return "WrappedIosPort_"; -}; - -/** - * Emulates the HTML 5 addEventListener interface. - * @param {string} eventName - * @param {function({data: Object})} handler - */ -u2f.WrappedIosPort_.prototype.addEventListener = function(eventName, handler) { - var name = eventName.toLowerCase(); - if (name !== 'message') { - console.error('WrappedIosPort only supports message'); - } -}; - -/** - * Sets up an embedded trampoline iframe, sourced from the extension. - * @param {function(MessagePort)} callback - * @private - */ -u2f.getIframePort_ = function(callback) { - // Create the iframe - var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID; - var iframe = document.createElement('iframe'); - iframe.src = iframeOrigin + '/u2f-comms.html'; - iframe.setAttribute('style', 'display:none'); - document.body.appendChild(iframe); - - var channel = new MessageChannel(); - var ready = function(message) { - if (message.data == 'ready') { - channel.port1.removeEventListener('message', ready); - callback(channel.port1); - } else { - console.error('First event on iframe port was not "ready"'); - } - }; - channel.port1.addEventListener('message', ready); - channel.port1.start(); - - iframe.addEventListener('load', function() { - // Deliver the port to the iframe and initialize - iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]); - }); -}; - - -//High-level JS API - -/** - * Default extension response timeout in seconds. - * @const - */ -u2f.EXTENSION_TIMEOUT_SEC = 30; - -/** - * A singleton instance for a MessagePort to the extension. - * @type {MessagePort|u2f.WrappedChromeRuntimePort_} - * @private - */ -u2f.port_ = null; - -/** - * Callbacks waiting for a port - * @type {Array} - * @private - */ -u2f.waitingForPort_ = []; - -/** - * A counter for requestIds. - * @type {number} - * @private - */ -u2f.reqCounter_ = 0; - -/** - * A map from requestIds to client callbacks - * @type {Object.} - * @private - */ -u2f.callbackMap_ = {}; - -/** - * Creates or retrieves the MessagePort singleton to use. - * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback - * @private - */ -u2f.getPortSingleton_ = function(callback) { - if (u2f.port_) { - callback(u2f.port_); - } else { - if (u2f.waitingForPort_.length == 0) { - u2f.getMessagePort(function(port) { - u2f.port_ = port; - u2f.port_.addEventListener('message', - /** @type {function(Event)} */ (u2f.responseHandler_)); - - // Careful, here be async callbacks. Maybe. - while (u2f.waitingForPort_.length) - u2f.waitingForPort_.shift()(u2f.port_); - }); - } - u2f.waitingForPort_.push(callback); - } -}; - -/** - * Handles response messages from the extension. - * @param {MessageEvent.} message - * @private - */ -u2f.responseHandler_ = function(message) { - var response = message.data; - var reqId = response['requestId']; - if (!reqId || !u2f.callbackMap_[reqId]) { - console.error('Unknown or missing requestId in response.'); - return; - } - var cb = u2f.callbackMap_[reqId]; - delete u2f.callbackMap_[reqId]; - cb(response['responseData']); -}; - -/** - * Dispatches an array of sign requests to available U2F tokens. - * If the JS API version supported by the extension is unknown, it first sends a - * message to the extension to find out the supported API version and then it sends - * the sign request. - * @param {string=} appId - * @param {string=} challenge - * @param {Array} registeredKeys - * @param {function((u2f.Error|u2f.SignResponse))} callback - * @param {number=} opt_timeoutSeconds - */ -u2f.sign = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { - if (js_api_version === undefined) { - // Send a message to get the extension to JS API version, then send the actual sign request. - u2f.getApiVersion( - function (response) { - js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version']; - console.log("Extension JS API Version: ", js_api_version); - u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); - }); - } else { - // We know the JS API version. Send the actual sign request in the supported API version. - u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); - } -}; - -/** - * Dispatches an array of sign requests to available U2F tokens. - * @param {string=} appId - * @param {string=} challenge - * @param {Array} registeredKeys - * @param {function((u2f.Error|u2f.SignResponse))} callback - * @param {number=} opt_timeoutSeconds - */ -u2f.sendSignRequest = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { - u2f.getPortSingleton_(function(port) { - var reqId = ++u2f.reqCounter_; - u2f.callbackMap_[reqId] = callback; - var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? - opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); - var req = u2f.formatSignRequest_(appId, challenge, registeredKeys, timeoutSeconds, reqId); - port.postMessage(req); - }); -}; - -/** - * Dispatches register requests to available U2F tokens. An array of sign - * requests identifies already registered tokens. - * If the JS API version supported by the extension is unknown, it first sends a - * message to the extension to find out the supported API version and then it sends - * the register request. - * @param {string=} appId - * @param {Array} registerRequests - * @param {Array} registeredKeys - * @param {function((u2f.Error|u2f.RegisterResponse))} callback - * @param {number=} opt_timeoutSeconds - */ -u2f.register = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { - if (js_api_version === undefined) { - // Send a message to get the extension to JS API version, then send the actual register request. - u2f.getApiVersion( - function (response) { - js_api_version = response['js_api_version'] === undefined ? 0: response['js_api_version']; - console.log("Extension JS API Version: ", js_api_version); - u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, - callback, opt_timeoutSeconds); - }); - } else { - // We know the JS API version. Send the actual register request in the supported API version. - u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, - callback, opt_timeoutSeconds); - } -}; - -/** - * Dispatches register requests to available U2F tokens. An array of sign - * requests identifies already registered tokens. - * @param {string=} appId - * @param {Array} registerRequests - * @param {Array} registeredKeys - * @param {function((u2f.Error|u2f.RegisterResponse))} callback - * @param {number=} opt_timeoutSeconds - */ -u2f.sendRegisterRequest = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { - u2f.getPortSingleton_(function(port) { - var reqId = ++u2f.reqCounter_; - u2f.callbackMap_[reqId] = callback; - var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? - opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); - var req = u2f.formatRegisterRequest_( - appId, registeredKeys, registerRequests, timeoutSeconds, reqId); - port.postMessage(req); - }); -}; - - -/** - * Dispatches a message to the extension to find out the supported - * JS API version. - * If the user is on a mobile phone and is thus using Google Authenticator instead - * of the Chrome extension, don't send the request and simply return 0. - * @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback - * @param {number=} opt_timeoutSeconds - */ -u2f.getApiVersion = function(callback, opt_timeoutSeconds) { - u2f.getPortSingleton_(function(port) { - // If we are using Android Google Authenticator or iOS client app, - // do not fire an intent to ask which JS API version to use. - if (port.getPortType) { - var apiVersion; - switch (port.getPortType()) { - case 'WrappedIosPort_': - case 'WrappedAuthenticatorPort_': - apiVersion = 1.1; - break; - - default: - apiVersion = 0; - break; - } - callback({ 'js_api_version': apiVersion }); - return; - } - var reqId = ++u2f.reqCounter_; - u2f.callbackMap_[reqId] = callback; - var req = { - type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST, - timeoutSeconds: (typeof opt_timeoutSeconds !== 'undefined' ? - opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC), - requestId: reqId - }; - port.postMessage(req); - }); -};