Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/twofactor_u2f.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristoph Wurst <ChristophWurst@users.noreply.github.com>2017-04-10 16:42:03 +0300
committerGitHub <noreply@github.com>2017-04-10 16:42:03 +0300
commiteb79826642d8e306b16a7267aebca3f547e2e0bb (patch)
tree9ecf7fae766b6b451fd2279176ae7ed5e0acc652
parent1d7d5f07871f764ff4cfb25ae66c247f9841a06e (diff)
parentb52b6d1469080085658278f1a86c748f7c5a718a (diff)
Merge pull request #40 from nextcloud/feature/register-multiple-devicesnightly-20170410
Add possibility to add multiple U2F devices
-rw-r--r--.travis.yml6
-rw-r--r--appinfo/database.xml7
-rw-r--r--appinfo/info.xml2
-rw-r--r--appinfo/routes.php10
-rw-r--r--css/style.css31
-rw-r--r--js/settingsview.js196
-rw-r--r--js/tests/spec/settingsviewSpec.js67
-rw-r--r--js/tests/test-main.js10
-rw-r--r--lib/Controller/SettingsController.php28
-rw-r--r--lib/Db/Registration.php4
-rw-r--r--lib/Db/RegistrationMapper.php4
-rw-r--r--lib/Provider/U2FProvider.php2
-rw-r--r--lib/Service/U2FManager.php45
-rw-r--r--templates/personal.php5
-rw-r--r--tests/unit/Activity/ProviderTest.php4
-rw-r--r--tests/unit/Activity/SettingTest.php4
-rw-r--r--tests/unit/Controller/SettingsControllerTest.php32
-rw-r--r--tests/unit/Provider/U2FProviderTest.php22
-rw-r--r--tests/unit/Service/U2FManagerTest.php23
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() {