diff options
author | Christoph Wurst <ChristophWurst@users.noreply.github.com> | 2017-04-10 16:42:03 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-04-10 16:42:03 +0300 |
commit | eb79826642d8e306b16a7267aebca3f547e2e0bb (patch) | |
tree | 9ecf7fae766b6b451fd2279176ae7ed5e0acc652 | |
parent | 1d7d5f07871f764ff4cfb25ae66c247f9841a06e (diff) | |
parent | b52b6d1469080085658278f1a86c748f7c5a718a (diff) |
Merge pull request #40 from nextcloud/feature/register-multiple-devicesnightly-20170410
Add possibility to add multiple U2F devices
-rw-r--r-- | .travis.yml | 6 | ||||
-rw-r--r-- | appinfo/database.xml | 7 | ||||
-rw-r--r-- | appinfo/info.xml | 2 | ||||
-rw-r--r-- | appinfo/routes.php | 10 | ||||
-rw-r--r-- | css/style.css | 31 | ||||
-rw-r--r-- | js/settingsview.js | 196 | ||||
-rw-r--r-- | js/tests/spec/settingsviewSpec.js | 67 | ||||
-rw-r--r-- | js/tests/test-main.js | 10 | ||||
-rw-r--r-- | lib/Controller/SettingsController.php | 28 | ||||
-rw-r--r-- | lib/Db/Registration.php | 4 | ||||
-rw-r--r-- | lib/Db/RegistrationMapper.php | 4 | ||||
-rw-r--r-- | lib/Provider/U2FProvider.php | 2 | ||||
-rw-r--r-- | lib/Service/U2FManager.php | 45 | ||||
-rw-r--r-- | templates/personal.php | 5 | ||||
-rw-r--r-- | tests/unit/Activity/ProviderTest.php | 4 | ||||
-rw-r--r-- | tests/unit/Activity/SettingTest.php | 4 | ||||
-rw-r--r-- | tests/unit/Controller/SettingsControllerTest.php | 32 | ||||
-rw-r--r-- | tests/unit/Provider/U2FProviderTest.php | 22 | ||||
-rw-r--r-- | tests/unit/Service/U2FManagerTest.php | 23 |
19 files changed, 335 insertions, 167 deletions
diff --git a/.travis.yml b/.travis.yml index 310b73d..ef15894 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,6 +38,10 @@ cache: before_install: - php --info + # Download phpunit 5.7 + - wget https://phar.phpunit.de/phpunit-5.7.phar -O phpunit + - chmod u+x phpunit + # XDebug is only needed if we report coverage -> speeds up other builds - if [[ "$PHP_COVERAGE" = "FALSE" ]]; then phpenv config-rm xdebug.ini; fi @@ -74,7 +78,7 @@ script: # Run PHP tests - cd tests - - phpunit --configuration phpunit.xml + - ../phpunit --configuration phpunit.xml # Publish PHP coverage to scrutinizer - if [[ "$PHP_COVERAGE" = "TRUE" ]]; then wget https://scrutinizer-ci.com/ocular.phar; fi diff --git a/appinfo/database.xml b/appinfo/database.xml index 629524b..04ee794 100644 --- a/appinfo/database.xml +++ b/appinfo/database.xml @@ -46,10 +46,15 @@ <notnull>true</notnull> <length>4</length> </field> + <field> + <name>name</name> + <type>text</type> + <notnull>false</notnull> + <length>255</length> + </field> <index> <name>u2f_registrations_user_id</name> - <unique>true</unique> <field> <name>user_id</name> <sorting>ascending</sorting> diff --git a/appinfo/info.xml b/appinfo/info.xml index 214d28c..4cee48f 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -4,7 +4,7 @@ <name>Two Factor U2F</name> <summary>U2F two-factor provider</summary> <description>A two-factor provider for U2F devices</description> - <version>1.2.0</version> + <version>1.3.0</version> <licence>agpl</licence> <author>Christoph Wurst</author> <namespace>TwoFactorU2F</namespace> diff --git a/appinfo/routes.php b/appinfo/routes.php index 5783439..f02b923 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -26,11 +26,6 @@ return [ 'verb' => 'GET' ], [ - 'name' => 'settings#disable', - 'url' => '/settings/disable', - 'verb' => 'POST' - ], - [ 'name' => 'settings#startRegister', 'url' => '/settings/startregister', 'verb' => 'POST' @@ -40,5 +35,10 @@ return [ 'url' => '/settings/finishregister', 'verb' => 'POST' ], + [ + 'name' => 'settings#remove', + 'url' => '/settings/remove', + 'verb' => 'POST' + ], ] ]; diff --git a/css/style.css b/css/style.css index 26ca70c..c0cf436 100644 --- a/css/style.css +++ b/css/style.css @@ -21,6 +21,33 @@ } /** icons for personal page settings **/ -.nav-icon-u2f-second-factor-auth { +.nav-icon-u2f-second-factor-auth, .icon-u2f-device { background-image: url('../img/app-dark.svg?v=1'); -}
\ No newline at end of file +} + +.u2f-device { + line-height: 300%; + display: flex; +} +.u2f-device .more { + position: relative; +} +.u2f-device .more .icon-more { + display: inline-block; + width: 16px; + height: 16px; + padding-left: 20px; + vertical-align: middle; + opacity: .7; +} +.u2f-device .popovermenu { + right: -5px; + top: 42px; +} + +.icon-u2f-device { + display: inline-block; + background-size: 100%; + padding: 3px; + margin: 3px; +} diff --git a/js/settingsview.js b/js/settingsview.js index 3ee2b2b..f4c9335 100644 --- a/js/settingsview.js +++ b/js/settingsview.js @@ -1,6 +1,6 @@ -/* global Backbone, Handlebars, OC, u2f, Promise */ +/* global Backbone, Handlebars, OC, u2f, Promise, _ */ -(function (OC, OCA, Backbone, Handlebars, $, u2f) { +(function (OC, OCA, Backbone, Handlebars, $, _, u2f) { 'use strict'; OCA.TwoFactorU2F = OCA.TwoFactorU2F || {}; @@ -8,18 +8,42 @@ var TEMPLATE = '' + '<div>' + ' {{#unless loading}}' - + ' <input type="checkbox" class="checkbox" id="u2f-enabled" {{#if enabled}}checked{{/if}}>' - + ' <label for="u2f-enabled">' + t('twofactor_u2f', 'Use U2F device') + '</label>' + + ' <div>' + + ' {{#unless devices.length}}' + + ' <span>' + t('twofactor_u2f', 'No U2F devices configured. You are not using U2F as second factor at the moment.') + '</span>' + + ' {{else}}' + + ' <span>' + t('twofactor_u2f', 'The following devices are configured for U2F second-factor authentication:') + '</span>' + + ' {{/unless}}' + + ' {{#each devices}}' + + ' <div class="u2f-device" data-u2f-id="{{id}}">' + + ' <span class="icon-u2f-device"></span>' + + ' <span>{{#if name}}{{name}}{{else}}' + t('twofactor_u2f', 'Unnamed device') + '{{/if}}</span>' + + ' <span class="more">' + + ' <a class="icon icon-more"></a>' + + ' <div class="popovermenu">' + + ' <ul>' + + ' <li class="remove-device">' + + ' <a><span class="icon-delete"></span><span>' + t('twofactor_u2f', 'Remove') + '</span></a>' + + ' </li>' + + ' </ul>' + + ' </div>' + + ' </span>' + + ' </div>' + + ' {{/each}}' + + ' </div>' + + ' <input id="u2f-device-name" type="text" placeholder="Name your device">' + + ' <button id="add-u2f-device">' + t('twofactor_u2f', 'Add U2F device') + '</button><br>' + + ' <span><small>' + t('twofactor_u2f', 'You can add as many devices as you like. It is recommended to give each device a distinct name.') + '</small></span>' + ' {{else}}' - + ' <span class="icon-loading-small u2f-loading"></span>' - + ' <span>' + t('twofactor_u2f', 'Use U2F device') + '</span>' + + ' <span class="icon-loading-small u2f-loading"></span>' + + ' <span>' + t('twofactor_u2f', 'Adding a new device …') + '</span>' + ' {{/unless}}' + '</div>'; /** - * @class + * @class */ - var SettingsView = Backbone.View.extend({ + var SettingsView = Backbone.View.extend(/** @lends Backbone.View */ { /** * @type {function|undefined} @@ -29,12 +53,12 @@ /** * @type {boolean} */ - _enabled: false, + _loading: false, /** - * @type {boolean} + * @type {Object[]} */ - _loading: false, + _devices: undefined, /** * @param {object} data @@ -48,17 +72,31 @@ }, events: { - 'change #u2f-enabled': '_onToggleEnabled' + 'click #add-u2f-device': '_onAddU2FDevice', + 'keydown #u2f-device-name': '_onInputKeyDown', + 'click .u2f-device .remove-device': '_onRemoveDevice' }, /** * @returns {undefined} */ render: function () { + this._devices = _.sortBy(this._devices, function (device) { + // Underscore's stable sort requires a value for each item + return device.name || ''; + }); + this.$el.html(this.template({ - enabled: this._enabled, - loading: this._loading + loading: this._loading, + devices: this._devices })); + + _.each(this._devices, function (device) { + var $deviceEl = this.$('div[data-u2f-id="' + device.id + '"]'); + OC.registerMenu($deviceEl.find('a.icon-more'), $deviceEl.find('.popovermenu')); + }, this); + + return this; }, /** @@ -77,10 +115,10 @@ */ load: function () { return this._getServerState().then(function (data) { - this._enabled = data.enabled; + this._devices = data.devices; this.render(); - }.bind(this)).catch(function (e) { - OC.Notification.showTemporary('Could not get U2F enabled/disabled state.'); + }.bind(this), function () { + OC.Notification.showTemporary('Could not load list of U2F devices.'); }).catch(console.error.bind(this)); }, @@ -88,62 +126,56 @@ * @private * @returns {Promise} */ - _onToggleEnabled: function () { + _onAddU2FDevice: function () { if (this._loading) { // Ignore event return Promise.resolve(); } - var enabled = this.$('#u2f-enabled').is(':checked'); - - if (enabled === this._enabled) { - return Promise.resolve(); - } - this._enabled = enabled; - - if (enabled) { - return this._onRegister(); - } else { - return this._onDisable(); - } + return this._onRegister(); }, /** * @private - * @returns {Promise} + * @param {Event} e */ - _onRegister: function () { - this._loading = true; - this.render(); - - var self = this; - return this._requirePasswordConfirmation() - .then(this._startRegistrationOnServer) - .then(function (data) { - return self._registerU2fDevice(data.req, data.sigs); - }) - .then(this._finishRegisterOnServer) - .catch(function (e) { - OC.Notification.showTemporary(e.message); - self._enabled = false; - }) - .then(function () { - self._loading = false; - self.render(); - }); + _onInputKeyDown: function (e) { + if (e.which === 13) { + return this._onAddU2FDevice(); + } + return Promise.resolve(); }, /** * @private * @returns {Promise} */ - _startRegistrationOnServer: function () { - var url = OC.generateUrl('/apps/twofactor_u2f/settings/startregister'); - return Promise.resolve($.ajax(url, { - method: 'POST' - })).catch(function (e) { + _onRemoveDevice: function (e) { + var deviceId = $(e.target).closest('.u2f-device').data('u2f-id'); + var device = _.find(this._devices, function (device) { + return device.id === deviceId; + }, this); + if (!device) { + console.error('Cannot remove u2f device: unkown'); + return Promise.reject('Unknown u2f device'); + } + + return this._requirePasswordConfirmation().then(function () { + // Remove visually + this._devices.splice(this._devices.indexOf(device), 1); + this.render(); + + // Remove on server + return this._removeOnServer(device); + }.bind(this)).catch(function (e) { + this._devices.push(device); + this.render(); console.error(e); - throw new Error(t('twofactor_u2f', 'Server error while trying to add U2F device')); + OC.Notification.showTemporary(t('twofactor_u2f', 'Could not remove your U2F device')); + throw new Error('Could not remove u2f device on server'); + }.bind(this)).catch(function (e) { + console.error('Unexpected error while removing the u2f device', e); + throw e; }); }, @@ -151,15 +183,28 @@ * @private * @returns {Promise} */ - _onDisable: function () { + _onRegister: function () { + var name = this.$('#u2f-device-name').val(); + + // Show loading feedback this._loading = true; this.render(); var self = this; return this._requirePasswordConfirmation() - .then(this._disableU2fOnServer) + .then(this._startRegistrationOnServer) + .then(function (data) { + return self._registerU2fDevice(data.req, data.sigs); + }) + .then(function (data) { + data.name = name; + return self._finishRegisterOnServer(data); + }) + .then(function (newDevice) { + self._devices.push(newDevice); + }) .catch(function (e) { - OC.Notification.showTemporary(e); + OC.Notification.showTemporary(e.message); }) .then(function () { self._loading = false; @@ -171,13 +216,13 @@ * @private * @returns {Promise} */ - _disableU2fOnServer: function () { - var url = OC.generateUrl('apps/twofactor_u2f/settings/disable'); + _startRegistrationOnServer: function () { + var url = OC.generateUrl('/apps/twofactor_u2f/settings/startregister'); return Promise.resolve($.ajax(url, { method: 'POST' })).catch(function (e) { console.error(e); - throw new Error(t('twofactor_u2f', 'Server error while disabling U2F')); + throw new Error(t('twofactor_u2f', 'Server error while trying to add U2F device')); }); }, @@ -223,7 +268,8 @@ /** * @private - * @param {object} data + * @param {Object} data + * @param {string} data.name device name (specified by the user) * @returns {Promise} */ _finishRegisterOnServer: function (data) { @@ -234,12 +280,32 @@ })).catch(function (e) { console.error(e); throw new Error(t('twofactor_u2f', 'Server error while trying to complete U2F device registration')); - }).then(function () { + }).then(function (data) { $('.utf-register-info').slideUp(); + return data; + }); + }, + + /** + * @private + * @param {Object} device + * @returns {Promise} + */ + _removeOnServer: function (device) { + var url = OC.generateUrl('/apps/twofactor_u2f/settings/remove'); + + return Promise.resolve($.ajax(url, { + method: 'POST', + data: { + id: device.id + } + })).catch(function (e) { + console.error(e); + throw e; }); } }); OCA.TwoFactorU2F.SettingsView = SettingsView; -})(OC, OCA, OC.Backbone, Handlebars, $, u2f); +})(OC, OCA, OC.Backbone, Handlebars, $, _, u2f); diff --git a/js/tests/spec/settingsviewSpec.js b/js/tests/spec/settingsviewSpec.js index ac83cbd..4537087 100644 --- a/js/tests/spec/settingsviewSpec.js +++ b/js/tests/spec/settingsviewSpec.js @@ -29,31 +29,6 @@ describe('Settings view', function () { }); expect(OC.Notification.showTemporary).not.toHaveBeenCalled(); - expect(view.$el.find('#u2f-enabled').prop('checked')).toBeUndefined(); - }); - - it('ticks the checkbox if u2f is enabled for the user', function (done) { - spyOn(OC.Notification, 'showTemporary'); - - var loading = view.load(); - - expect(jasmine.Ajax.requests.mostRecent().url).toBe('/apps/twofactor_u2f/settings/state'); - - jasmine.Ajax.requests.mostRecent().respondWith({ - status: 200, - contentType: 'application/json', - responseText: JSON.stringify({ - enabled: true - }) - }); - - loading.then(function () { - expect(OC.Notification.showTemporary).not.toHaveBeenCalled(); - expect(view.$el.find('#u2f-enabled').prop('checked')).toBe(true); - done(); - }).catch(function (e) { - done.fail(e); - }); }); it('shows a notification if the state cannot be loaded from the server', function (done) { @@ -79,15 +54,14 @@ describe('Settings view', function () { it('asks for password confirmation when the user enables u2f', function (done) { spyOn(OC.Notification, 'showTemporary'); spyOn(view, '_getServerState').and.returnValue(Promise.resolve({ - enabled: false + devices: [] })); spyOn(view, '_requirePasswordConfirmation').and.returnValue(Promise.reject({ message: 'Wrong password' })); view.load().then(function () { - view.$el.find('#u2f-enabled').prop('checked', true); - view._onToggleEnabled().then(function () { + view._onAddU2FDevice().then(function () { expect(OC.Notification.showTemporary).toHaveBeenCalledWith('Wrong password'); done(); }).catch(function (e) { @@ -101,9 +75,9 @@ describe('Settings view', function () { it('lets the user register a new device', function (done) { spyOn(OC.Notification, 'showTemporary'); spyOn(view, '_getServerState').and.returnValue(Promise.resolve({ - enabled: false + devices: [] })); - spyOn(view, '_registerU2fDevice').and.returnValue(Promise.resolve()); + spyOn(view, '_registerU2fDevice').and.returnValue(Promise.resolve({})); spyOn(view, '_requirePasswordConfirmation').and.returnValue(Promise.resolve()); jasmine.Ajax.stubRequest('/apps/twofactor_u2f/settings/startregister').andReturn({ contentType: 'application/json', @@ -118,9 +92,8 @@ describe('Settings view', function () { }); view.load().then(function () { - view.$el.find('#u2f-enabled').prop('checked', true); expect(view._getServerState).toHaveBeenCalled(); - return view._onToggleEnabled().then(function () { + return view._onAddU2FDevice().then(function () { expect(view._registerU2fDevice).toHaveBeenCalled(); expect(OC.Notification.showTemporary).not.toHaveBeenCalled(); done(); @@ -130,4 +103,34 @@ describe('Settings view', function () { }); }); + it('lets the user remove a device', function (done) { + spyOn(OC.Notification, 'showTemporary'); + spyOn(view, '_getServerState').and.returnValue(Promise.resolve({ + devices: [ + { + id: 13, + name: 'Yolokey' + } + ] + })); + spyOn(view, '_requirePasswordConfirmation').and.returnValue(Promise.resolve()); + jasmine.Ajax.stubRequest('/apps/twofactor_u2f/settings/remove').andReturn({ + contentType: 'application/json', + responseText: JSON.stringify({}) + }); + + view.load().then(function () { + expect(view._getServerState).toHaveBeenCalled(); + var fakeEvent = { + target: view.$('.remove-device') + }; + return view._onRemoveDevice(fakeEvent).then(function () { + expect(OC.Notification.showTemporary).not.toHaveBeenCalled(); + done(); + }); + }).catch(function (e) { + done.fail(e); + }); + }); + }); diff --git a/js/tests/test-main.js b/js/tests/test-main.js index 5d96d85..acb8a44 100644 --- a/js/tests/test-main.js +++ b/js/tests/test-main.js @@ -1,12 +1,16 @@ -(function (global) { +(function (global, Backbone) { // Global variable stubs global.OC = {}; global.OC.generateUrl = function (url) { return url; }; + global.OC.Backbone = Backbone; global.OC.Notification = {}; - global.OC.Notification.showTemporary = function () { + global.OC.Notification.showTemporary = function (txt) { + console.error('temporary notification', txt) + }; + global.OC.registerMenu = function () { }; global.OCA = {}; @@ -16,4 +20,4 @@ } return txt; }; -})(window); +})(window, Backbone); diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php index bcd8992..d55dbc3 100644 --- a/lib/Controller/SettingsController.php +++ b/lib/Controller/SettingsController.php @@ -16,6 +16,7 @@ require_once(__DIR__ . '/../../vendor/yubico/u2flib-server/src/u2flib_server/U2F use OCA\TwoFactorU2F\Service\U2FManager; use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\JSONResponse; use OCP\IRequest; use OCP\IUserSession; @@ -41,39 +42,46 @@ class SettingsController extends Controller { /** * @NoAdminRequired + * @return JSONResponse */ public function state() { return [ - 'enabled' => $this->manager->isEnabled($this->userSession->getUser()) + 'devices' => $this->manager->getDevices($this->userSession->getUser()) ]; } /** * @NoAdminRequired * @PasswordConfirmationRequired + * @UseSession + * @return JSONResponse */ - public function disable() { - $this->manager->disableU2F($this->userSession->getUser()); + public function startRegister() { + return $this->manager->startRegistration($this->userSession->getUser()); } /** * @NoAdminRequired * @PasswordConfirmationRequired - * @UseSession + * + * @param string $registrationData + * @param string $clientData + * @param string|null $name device name, given by user + * @return JSONResponse */ - public function startRegister() { - return $this->manager->startRegistration($this->userSession->getUser()); + public function finishRegister($registrationData, $clientData, $name = null) { + return $this->manager->finishRegistration($this->userSession->getUser(), $registrationData, $clientData, $name); } /** * @NoAdminRequired * @PasswordConfirmationRequired * - * @param string $registrationData - * @param string $clientData + * @param int $id + * @return JSONResponse */ - public function finishRegister($registrationData, $clientData) { - $this->manager->finishRegistration($this->userSession->getUser(), $registrationData, $clientData); + public function remove($id) { + return $this->manager->removeDevice($this->userSession->getUser(), $id); } } diff --git a/lib/Db/Registration.php b/lib/Db/Registration.php index b19370b..306ebbf 100644 --- a/lib/Db/Registration.php +++ b/lib/Db/Registration.php @@ -26,6 +26,8 @@ use OCP\AppFramework\Db\Entity; * @method void setCertificate(string $Certificate) * @method int getCounter() * @method void setCounter(int $counter) + * @method string getName() + * @method void setName(string $name) */ class Registration extends Entity implements JsonSerializable { @@ -34,6 +36,7 @@ class Registration extends Entity implements JsonSerializable { protected $publicKey; protected $certificate; protected $counter; + protected $name; public function jsonSerialize() { return [ @@ -43,6 +46,7 @@ class Registration extends Entity implements JsonSerializable { 'publicKey' => $this->getPublicKey(), 'certificate' => $this->getCertificate(), 'counter' => $this->getCounter(), + 'name' => $this->getName(), ]; } diff --git a/lib/Db/RegistrationMapper.php b/lib/Db/RegistrationMapper.php index 8acf8da..766bc78 100644 --- a/lib/Db/RegistrationMapper.php +++ b/lib/Db/RegistrationMapper.php @@ -32,7 +32,7 @@ class RegistrationMapper extends Mapper { /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); - $qb->select('id', 'user_id', 'key_handle', 'public_key', 'certificate', 'counter') + $qb->select('id', 'user_id', 'key_handle', 'public_key', 'certificate', 'counter', 'name') ->from('twofactor_u2f_registrations') ->where($qb->expr()->eq('user_id', $qb->createNamedParameter($user->getUID()))) ->andWhere($qb->expr()->eq('id', $qb->createNamedParameter($id))); @@ -52,7 +52,7 @@ class RegistrationMapper extends Mapper { /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); - $qb->select('id', 'user_id', 'key_handle', 'public_key', 'certificate', 'counter') + $qb->select('id', 'user_id', 'key_handle', 'public_key', 'certificate', 'counter', 'name') ->from('twofactor_u2f_registrations') ->where($qb->expr()->eq('user_id', $qb->createNamedParameter($user->getUID()))); $result = $qb->execute(); diff --git a/lib/Provider/U2FProvider.php b/lib/Provider/U2FProvider.php index 6a182a3..887f114 100644 --- a/lib/Provider/U2FProvider.php +++ b/lib/Provider/U2FProvider.php @@ -93,7 +93,7 @@ class U2FProvider implements IProvider { * @return boolean */ public function isTwoFactorAuthEnabledForUser(IUser $user) { - return $this->manager->isEnabled($user); + return count($this->manager->getDevices($user)) > 0; } } diff --git a/lib/Service/U2FManager.php b/lib/Service/U2FManager.php index a7c1a29..d04ae43 100644 --- a/lib/Service/U2FManager.php +++ b/lib/Service/U2FManager.php @@ -70,19 +70,34 @@ class U2FManager { return $registrationObjects; } - public function isEnabled(IUser $user) { + /** + * @param IUser $user + * @return array + */ + public function getDevices(IUser $user) { $registrations = $this->mapper->findRegistrations($user); - return count($registrations) > 0; + return array_map(function(Registration $reg) { + return [ + 'id' => $reg->getId(), + 'name' => $reg->getName(), + ]; + }, $registrations); } - public function disableU2F(IUser $user) { - // TODO: use single query instead - foreach ($this->mapper->findRegistrations($user) as $registration) { - $this->mapper->delete($registration); - $this->publishEvent($user, 'u2f_device_removed'); - } + /** + * @param IUser $user + * @param int $id device id + */ + public function removeDevice(IUser $user, $id) { + $reg = $this->mapper->findRegistration($user, $id); + $this->mapper->delete($reg); + $this->publishEvent($user, 'u2f_device_removed'); } + /** + * @param IUser $user + * @return array + */ public function startRegistration(IUser $user) { $u2f = $this->getU2f(); $data = $u2f->getRegisterData($this->getRegistrations($user)); @@ -99,7 +114,13 @@ class U2FManager { ]; } - public function finishRegistration(IUser $user, $registrationData, $clientData) { + /** + * @param IUser $user + * @param string $registrationData + * @param string $clientData + * @param string $name + */ + public function finishRegistration(IUser $user, $registrationData, $clientData, $name = null) { $this->logger->debug($registrationData); $this->logger->debug($clientData); @@ -117,10 +138,16 @@ class U2FManager { $registration->setPublicKey($reg->publicKey); $registration->setCertificate($reg->certificate); $registration->setCounter($reg->counter); + $registration->setName($name); $this->mapper->insert($registration); $this->publishEvent($user, 'u2f_device_added'); $this->logger->debug(json_encode($reg)); + + return [ + 'id' => $registration->getId(), + 'name' => $registration->getName(), + ]; } /** diff --git a/templates/personal.php b/templates/personal.php index 28f48fb..db81e09 100644 --- a/templates/personal.php +++ b/templates/personal.php @@ -7,7 +7,10 @@ style('twofactor_u2f', 'style'); <div class="section"> <h2><?php p($l->t('U2F second-factor auth')); ?></h2> - <div id="twofactor-u2f-settings"></div> + <div id="twofactor-u2f-settings"> + <span class="icon-loading-small u2f-loading"></span> + <span><?php p($l->t('Loading your devices …')); ?></span> + </div> <p class="utf-register-info" style="display: none;"><?php p($l->t('Please plug in your U2F device and press the device button to authorize.')) ?></p> <p class="utf-register-info" style="display: none;"><em><?php p($l->t('Chrome is the only browser that supports U2F devices. You need to install the "U2F Support Add-on" on Firefox to use U2F.')) ?></em></p> <p class="utf-register-success" style="display: none;"><span class="icon-checkmark-color" style="width: 16px;"></span><?php p($l->t('U2F device successfully registered.')) ?></p> diff --git a/tests/unit/Activity/ProviderTest.php b/tests/unit/Activity/ProviderTest.php index 9ed15b3..a97a334 100644 --- a/tests/unit/Activity/ProviderTest.php +++ b/tests/unit/Activity/ProviderTest.php @@ -29,9 +29,9 @@ use OCP\IL10N; use OCP\ILogger; use OCP\IURLGenerator; use OCP\L10N\IFactory; -use Test\TestCase; +use PHPUnit_Framework_TestCase; -class ProviderTest extends TestCase { +class ProviderTest extends PHPUnit_Framework_TestCase { private $l10n; private $urlGenerator; diff --git a/tests/unit/Activity/SettingTest.php b/tests/unit/Activity/SettingTest.php index 89ea086..3797beb 100644 --- a/tests/unit/Activity/SettingTest.php +++ b/tests/unit/Activity/SettingTest.php @@ -24,9 +24,9 @@ namespace OCA\TwoFactorU2F\Tests\Unit\Activity; use OCA\TwoFactorU2F\Activity\Setting; use OCP\IL10N; -use Test\TestCase; +use PHPUnit_Framework_TestCase; -class SettingTest extends TestCase { +class SettingTest extends PHPUnit_Framework_TestCase { private $l10n; diff --git a/tests/unit/Controller/SettingsControllerTest.php b/tests/unit/Controller/SettingsControllerTest.php index 3962ae3..90a6236 100644 --- a/tests/unit/Controller/SettingsControllerTest.php +++ b/tests/unit/Controller/SettingsControllerTest.php @@ -18,9 +18,9 @@ use OCP\IRequest; use OCP\IUser; use OCP\IUserSession; use PHPUnit_Framework_MockObject_MockObject; -use Test\TestCase; +use PHPUnit_Framework_TestCase; -class SettingsControllerTest extends TestCase { +class SettingsControllerTest extends PHPUnit_Framework_TestCase { /** @var IRequest|PHPUnit_Framework_MockObject_MockObject */ private $request; @@ -46,32 +46,30 @@ class SettingsControllerTest extends TestCase { public function testState() { $user = $this->createMock(IUser::class); + $devices = [ + [ + 'id' => 1, + 'name' => null, + ], + [ + 'id' => 2, + 'name' => 'Yolokey', + ], + ]; $this->userSession->expects($this->once()) ->method('getUser') ->willReturn($user); $this->u2fManager->expects($this->once()) - ->method('isEnabled') + ->method('getDevices') ->with($this->equalTo($user)) - ->willReturn(true); + ->willReturn($devices); $expected = [ - 'enabled' => true, + 'devices' => $devices, ]; $this->assertSame($expected, $this->controller->state()); } - public function testDisable() { - $user = $this->createMock(IUser::class); - $this->userSession->expects($this->once()) - ->method('getUser') - ->willReturn($user); - $this->u2fManager->expects($this->once()) - ->method('disableU2F') - ->with($this->equalTo($user)); - - $this->controller->disable(); - } - public function testStartRegister() { $user = $this->createMock(IUser::class); $this->userSession->expects($this->once()) diff --git a/tests/unit/Provider/U2FProviderTest.php b/tests/unit/Provider/U2FProviderTest.php index aa41828..8d5885f 100644 --- a/tests/unit/Provider/U2FProviderTest.php +++ b/tests/unit/Provider/U2FProviderTest.php @@ -18,9 +18,9 @@ use OCP\IL10N; use OCP\IUser; use OCP\Template; use PHPUnit_Framework_MockObject_MockObject; -use Test\TestCase; +use PHPUnit_Framework_TestCase; -class U2FProviderTest extends TestCase { +class U2FProviderTest extends PHPUnit_Framework_TestCase { /** @var IL10N|PHPUnit_Framework_MockObject_MockObject */ private $l10n; @@ -84,10 +84,24 @@ class U2FProviderTest extends TestCase { public function testIsTwoFactorAuthEnabledForUser() { $user = $this->createMock(IUser::class); + $devices = [ + 'dev1', + ]; $this->manager->expects($this->once()) - ->method('isEnabled') - ->willReturn(false); + ->method('getDevices') + ->willReturn($devices); + + $this->assertTrue($this->provider->isTwoFactorAuthEnabledForUser($user)); + } + + public function testIsTwoFactorAuthDisabledForUser() { + $user = $this->createMock(IUser::class); + $devices = []; + + $this->manager->expects($this->once()) + ->method('getDevices') + ->willReturn($devices); $this->assertFalse($this->provider->isTwoFactorAuthEnabledForUser($user)); } diff --git a/tests/unit/Service/U2FManagerTest.php b/tests/unit/Service/U2FManagerTest.php index 549af93..0e183ee 100644 --- a/tests/unit/Service/U2FManagerTest.php +++ b/tests/unit/Service/U2FManagerTest.php @@ -22,10 +22,10 @@ use OCP\IRequest; use OCP\ISession; use OCP\IUser; use PHPUnit_Framework_MockObject_MockObject; -use Test\TestCase; +use PHPUnit_Framework_TestCase; use u2flib_server\U2F; -class U2FManagerTest extends TestCase { +class U2FManagerTest extends PHPUnit_Framework_TestCase { /** @var RegistrationMapper|PHPUnit_Framework_MockObject_MockObject */ private $mapper; @@ -86,27 +86,32 @@ class U2FManagerTest extends TestCase { ->willReturn($regs); } - public function testIsEnabled() { + public function testGetDevices() { $user = $this->createMock(IUser::class); $this->mockRegistrations($user, 2); - $this->assertTrue($this->manager->isEnabled($user)); + $this->assertCount(2, $this->manager->getDevices($user)); } - public function testIsEnabledDisabled() { + public function testGetNoDevices() { $user = $this->createMock(IUser::class); $this->mockRegistrations($user, 0); - $this->assertFalse($this->manager->isEnabled($user)); + $this->assertEmpty($this->manager->getDevices($user)); } public function testDisableU2F() { $user = $this->createMock(IUser::class); - $this->mockRegistrations($user, 1); $event = $this->createMock(IEvent::class); + $reg = $this->createMock(Registration::class); $this->mapper->expects($this->once()) - ->method('delete'); + ->method('findRegistration') + ->with($user, 13) + ->willReturn($reg); + $this->mapper->expects($this->once()) + ->method('delete') + ->with($reg); $this->activityManager->expects($this->once()) ->method('generateEvent') ->willReturn($event); @@ -137,7 +142,7 @@ class U2FManagerTest extends TestCase { ->method('publish') ->with($this->equalTo($event)); - $this->manager->disableU2F($user); + $this->manager->removeDevice($user, 13); } public function testStartRegistrationFirstDevice() { |