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

github.com/nextcloud/server.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristoph Wurst <christoph@winzerhof-wurst.at>2016-08-29 20:19:44 +0300
committerRoeland Jago Douma <roeland@famdouma.nl>2016-09-05 09:51:13 +0300
commit8acb734854484e2ffd235929f6e7d0ba4c273844 (patch)
tree3269bc6cc60b51d4fd507d91e8eca3a4ecc262cd
parent8b484eedf029b8e1a9dcef0efb09db381888c4b0 (diff)
add 2fa backup codes app
* add backup codes app unit tests * add integration tests for the backup codes app
-rw-r--r--.gitignore1
-rw-r--r--apps/twofactor_backupcodes/appinfo/app.php22
-rw-r--r--apps/twofactor_backupcodes/appinfo/database.xml48
-rw-r--r--apps/twofactor_backupcodes/appinfo/info.xml19
-rw-r--r--apps/twofactor_backupcodes/appinfo/routes.php35
-rw-r--r--apps/twofactor_backupcodes/css/style.css25
-rw-r--r--apps/twofactor_backupcodes/js/settings.js16
-rw-r--r--apps/twofactor_backupcodes/js/settingsview.js120
-rw-r--r--apps/twofactor_backupcodes/lib/Controller/SettingsController.php73
-rw-r--r--apps/twofactor_backupcodes/lib/Db/BackupCode.php46
-rw-r--r--apps/twofactor_backupcodes/lib/Db/BackupCodeMapper.php66
-rw-r--r--apps/twofactor_backupcodes/lib/Provider/BackupCodesProvider.php102
-rw-r--r--apps/twofactor_backupcodes/lib/Service/BackupCodeStorage.php121
-rw-r--r--apps/twofactor_backupcodes/settings/personal.php5
-rw-r--r--apps/twofactor_backupcodes/templates/challenge.php8
-rw-r--r--apps/twofactor_backupcodes/templates/personal.php12
-rw-r--r--apps/twofactor_backupcodes/tests/Integration/Db/BackupCodeMapperTest.php113
-rw-r--r--apps/twofactor_backupcodes/tests/Integration/Service/BackupCodeStorageTest.php90
-rw-r--r--apps/twofactor_backupcodes/tests/Unit/Controller/SettingsControllerTest.php95
-rw-r--r--apps/twofactor_backupcodes/tests/Unit/Provider/BackupCodesProviderTest.php103
-rw-r--r--apps/twofactor_backupcodes/tests/Unit/Service/BackupCodeStorageTest.php228
-rw-r--r--core/Controller/TwoFactorChallengeController.php9
-rw-r--r--core/css/styles.css2
-rw-r--r--core/shipped.json2
-rw-r--r--core/templates/twofactorselectchallenge.php10
-rw-r--r--core/templates/twofactorshowchallenge.php10
-rw-r--r--lib/private/Authentication/TwoFactorAuth/Manager.php20
-rw-r--r--tests/Core/Controller/TwoFactorChallengeControllerTest.php19
-rw-r--r--tests/lib/Authentication/TwoFactorAuth/ManagerTest.php84
29 files changed, 1487 insertions, 17 deletions
diff --git a/.gitignore b/.gitignore
index 0811a12f44b..56095af8308 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,6 +27,7 @@
!/apps/admin_audit
!/apps/updatenotification
!/apps/theming
+!/apps/twofactor_backupcodes
!/apps/workflowengine
/apps/files_external/3rdparty/irodsphp/PHPUnitTest
/apps/files_external/3rdparty/irodsphp/web
diff --git a/apps/twofactor_backupcodes/appinfo/app.php b/apps/twofactor_backupcodes/appinfo/app.php
new file mode 100644
index 00000000000..31f9b6b8eae
--- /dev/null
+++ b/apps/twofactor_backupcodes/appinfo/app.php
@@ -0,0 +1,22 @@
+<?php
+
+/**
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+OC_App::registerPersonal('twofactor_backupcodes', 'settings/personal');
diff --git a/apps/twofactor_backupcodes/appinfo/database.xml b/apps/twofactor_backupcodes/appinfo/database.xml
new file mode 100644
index 00000000000..02ca7c93a1e
--- /dev/null
+++ b/apps/twofactor_backupcodes/appinfo/database.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<database>
+ <name>*dbname*</name>
+ <create>true</create>
+ <overwrite>false</overwrite>
+ <charset>utf8</charset>
+ <table>
+ <name>*dbprefix*twofactor_backup_codes</name>
+ <declaration>
+ <field>
+ <name>id</name>
+ <type>integer</type>
+ <autoincrement>1</autoincrement>
+ <default>0</default>
+ <notnull>true</notnull>
+ <length>4</length>
+ </field>
+ <field>
+ <name>user_id</name>
+ <type>text</type>
+ <default></default>
+ <notnull>true</notnull>
+ <length>64</length>
+ </field>
+ <field>
+ <name>code</name>
+ <type>text</type>
+ <notnull>true</notnull>
+ <length>64</length>
+ </field>
+ <field>
+ <name>used</name>
+ <type>integer</type>
+ <notnull>true</notnull>
+ <default>0</default>
+ <length>1</length>
+ </field>
+
+ <index>
+ <name>two_factor_backupcodes_user_id</name>
+ <field>
+ <name>user_id</name>
+ <sorting>ascending</sorting>
+ </field>
+ </index>
+ </declaration>
+ </table>
+</database>
diff --git a/apps/twofactor_backupcodes/appinfo/info.xml b/apps/twofactor_backupcodes/appinfo/info.xml
new file mode 100644
index 00000000000..45e9b022bbf
--- /dev/null
+++ b/apps/twofactor_backupcodes/appinfo/info.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0"?>
+<info>
+ <id>twofactor_backupcodes</id>
+ <name>Two factor backup codes</name>
+ <description>A two-factor auth backup codes provider</description>
+ <licence>agpl</licence>
+ <author>Christoph Wurst</author>
+ <version>1.0.0</version>
+ <namespace>TwoFactor_BackupCodes</namespace>
+ <category>other</category>
+
+ <two-factor-providers>
+ <provider>OCA\TwoFactor_BackupCodes\Provider\BackupCodesProvider</provider>
+ </two-factor-providers>
+
+ <dependencies>
+ <owncloud min-version="9.2" max-version="9.2" />
+ </dependencies>
+</info>
diff --git a/apps/twofactor_backupcodes/appinfo/routes.php b/apps/twofactor_backupcodes/appinfo/routes.php
new file mode 100644
index 00000000000..f2af12e9b45
--- /dev/null
+++ b/apps/twofactor_backupcodes/appinfo/routes.php
@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+return [
+ 'routes' => [
+ [
+ 'name' => 'settings#state',
+ 'url' => '/settings/state',
+ 'verb' => 'GET'
+ ],
+ [
+ 'name' => 'settings#createCodes',
+ 'url' => '/settings/create',
+ 'verb' => 'POST'
+ ],
+ ]
+];
diff --git a/apps/twofactor_backupcodes/css/style.css b/apps/twofactor_backupcodes/css/style.css
new file mode 100644
index 00000000000..0e15290b86a
--- /dev/null
+++ b/apps/twofactor_backupcodes/css/style.css
@@ -0,0 +1,25 @@
+.challenge-form {
+ margin: 16px auto 1px !important;
+}
+
+.challenge {
+ margin-top: 0 !important;
+ margin-left: 0 !important;
+}
+
+.confirm-inline {
+ position: absolute;
+ right: 10px;
+ top: 0;
+ margin: 0 !important;
+ padding-right: 25px !important;
+ background-color: transparent !important;
+ border: none !important;
+ opacity: .5;
+}
+
+.backup-code {
+ font-family: monospace;
+ letter-spacing: 0.02em;
+ font-size: 1.2em;
+}
diff --git a/apps/twofactor_backupcodes/js/settings.js b/apps/twofactor_backupcodes/js/settings.js
new file mode 100644
index 00000000000..a4045cbc118
--- /dev/null
+++ b/apps/twofactor_backupcodes/js/settings.js
@@ -0,0 +1,16 @@
+/* global OC */
+
+(function (OC) {
+ 'use strict';
+
+ OC.Settings = OC.Settings || {};
+ OC.Settings.TwoFactorBackupCodes = OC.Settings.TwoFactorBackupCodes || {};
+
+ $(function () {
+ var view = new OC.Settings.TwoFactorBackupCodes.View({
+ el: $('#twofactor-backupcodes-settings')
+ });
+ view.render();
+ });
+})(OC);
+
diff --git a/apps/twofactor_backupcodes/js/settingsview.js b/apps/twofactor_backupcodes/js/settingsview.js
new file mode 100644
index 00000000000..3da4c141159
--- /dev/null
+++ b/apps/twofactor_backupcodes/js/settingsview.js
@@ -0,0 +1,120 @@
+/* global Backbone, Handlebars, OC, _ */
+
+(function (OC, Handlebars, $, _) {
+ 'use strict';
+
+ OC.Settings = OC.Settings || {};
+ OC.Settings.TwoFactorBackupCodes = OC.Settings.TwoFactorBackupCodes || {};
+
+ var TEMPLATE = '<div>'
+ + '{{#unless enabled}}'
+ + '<button id="generate-backup-codes">' + t('twofactor_backupcodes', 'Generate backup codes') + '</button>'
+ + '{{else}}'
+ + '<p>'
+ + '{{#unless codes}}'
+ + t('twofactor_backupcodes', 'Backup codes have been generated. {{used}} of {{total}} codes have been used.')
+ + '{{else}}'
+ + t('twofactor_backupcodes', 'These are your backup codes. Please save and/or print them as you will not be able to read the codes again later')
+ + '<ul>'
+ + '{{#each codes}}'
+ + '<li class="backup-code">{{this}}</li>'
+ + '{{/each}}'
+ + '</ul>'
+ + '<a href="{{download}}" class="button" download="Nextcloud-backup-codes.txt">' + t('twofactor_backupcodes', 'Save backup codes') + '</a>'
+ + '<button id="print-backup-codes" class="button">' + t('twofactor_backupcodes', 'Print backup codes') + '</button>'
+ + '{{/unless}}'
+ + '</p>'
+ + '<p>'
+ + '<button id="generate-backup-codes">' + t('twofactor_backupcodes', 'Regenerate backup codes') + '</button>'
+ + '</p>'
+ + '<p>'
+ + t('twofactor_backupcodes', 'If you regenerate backup codes, you automatically invalidate old codes.')
+ + '</p>'
+ + '{{/unless}}'
+ + '</div';
+
+ var View = OC.Backbone.View.extend({
+ _template: undefined,
+ template: function (data) {
+ if (!this._template) {
+ this._template = Handlebars.compile(TEMPLATE);
+ }
+ return this._template(data);
+ },
+ _loading: undefined,
+ _enabled: undefined,
+ _total: undefined,
+ _used: undefined,
+ _codes: undefined,
+ events: {
+ 'click #generate-backup-codes': '_onGenerateBackupCodes',
+ 'click #print-backup-codes': '_onPrintBackupCodes',
+ },
+ initialize: function () {
+ this._load();
+ },
+ render: function () {
+ this.$el.html(this.template({
+ enabled: this._enabled,
+ total: this._total,
+ used: this._used,
+ codes: this._codes,
+ download: this._getDownloadDataHref()
+ }));
+ },
+ _getDownloadDataHref: function () {
+ if (!this._codes) {
+ return '';
+ }
+ return 'data:text/plain,' + encodeURIComponent(_.reduce(this._codes, function (prev, code) {
+ return prev + code + "\r\n";
+ }, ''));
+ },
+ _load: function () {
+ this._loading = true;
+
+ var url = OC.generateUrl('/apps/twofactor_backupcodes/settings/state');
+ var loading = $.ajax(url, {
+ method: 'GET',
+ });
+
+ $.when(loading).done(function (data) {
+ this._enabled = data.enabled;
+ this._total = data.total;
+ this._used = data.used;
+ }.bind(this));
+ $.when(loading).always(function () {
+ this._loading = false;
+ this.render();
+ }.bind(this));
+ },
+ _onGenerateBackupCodes: function () {
+ // Hide old codes
+ this._enabled = false;
+ this.render();
+ $('#generate-backup-codes').addClass('icon-loading-small');
+ var url = OC.generateUrl('/apps/twofactor_backupcodes/settings/create');
+ $.ajax(url, {
+ method: 'POST'
+ }).done(function (data) {
+ this._enabled = data.state.enabled;
+ this._total = data.state.total;
+ this._used = data.state.used;
+ this._codes = data.codes;
+ this.render();
+ }.bind(this)).fail(function () {
+ OC.Notification.showTemporary('An error occurred while generating your backup codes');
+ $('#generate-backup-codes').removeClass('icon-loading-small');
+ });
+ },
+ _onPrintBackupCodes: function () {
+ var url = this._getDownloadDataHref();
+ window.open(url, 'Nextcloud backpu codes');
+ window.print();
+ window.close();
+ }
+ });
+
+ OC.Settings.TwoFactorBackupCodes.View = View;
+
+})(OC, Handlebars, $, _); \ No newline at end of file
diff --git a/apps/twofactor_backupcodes/lib/Controller/SettingsController.php b/apps/twofactor_backupcodes/lib/Controller/SettingsController.php
new file mode 100644
index 00000000000..5130357baa5
--- /dev/null
+++ b/apps/twofactor_backupcodes/lib/Controller/SettingsController.php
@@ -0,0 +1,73 @@
+<?php
+
+/**
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\TwoFactor_BackupCodes\Controller;
+
+use OCA\TwoFactor_BackupCodes\Service\BackupCodeStorage;
+use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http\JSONResponse;
+use OCP\IRequest;
+use OCP\IUserSession;
+
+class SettingsController extends Controller {
+
+ /** @var BackupCodeStorage */
+ private $storage;
+
+ /** @var IUserSession */
+ private $userSession;
+
+ /**
+ * @param string $appName
+ * @param IRequest $request
+ * @param BackupCodeStorage $storage
+ * @param IUserSession $userSession
+ */
+ public function __construct($appName, IRequest $request, BackupCodeStorage $storage, IUserSession $userSession) {
+ parent::__construct($appName, $request);
+ $this->userSession = $userSession;
+ $this->storage = $storage;
+ }
+
+ /**
+ * @NoAdminRequired
+ * @return JSONResponse
+ */
+ public function state() {
+ $user = $this->userSession->getUser();
+ return $this->storage->getBackupCodesState($user);
+ }
+
+ /**
+ * @NoAdminRequired
+ * @return JSONResponse
+ */
+ public function createCodes() {
+ $user = $this->userSession->getUser();
+ $codes = $this->storage->createCodes($user);
+ return [
+ 'codes' => $codes,
+ 'state' => $this->storage->getBackupCodesState($user),
+ ];
+ }
+
+}
diff --git a/apps/twofactor_backupcodes/lib/Db/BackupCode.php b/apps/twofactor_backupcodes/lib/Db/BackupCode.php
new file mode 100644
index 00000000000..5bfb681063c
--- /dev/null
+++ b/apps/twofactor_backupcodes/lib/Db/BackupCode.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\TwoFactor_BackupCodes\Db;
+
+use OCP\AppFramework\Db\Entity;
+
+/**
+ * @method string getUserId()
+ * @method void setUserId(string $userId)
+ * @method string getCode()
+ * @method void setCode(string $code)
+ * @method int getUsed()
+ * @method void setUsed(int $code)
+ */
+class BackupCode extends Entity {
+
+ /** @var string */
+ protected $userId;
+
+ /** @var string */
+ protected $code;
+
+ /** @var int */
+ protected $used;
+
+}
diff --git a/apps/twofactor_backupcodes/lib/Db/BackupCodeMapper.php b/apps/twofactor_backupcodes/lib/Db/BackupCodeMapper.php
new file mode 100644
index 00000000000..d6256929675
--- /dev/null
+++ b/apps/twofactor_backupcodes/lib/Db/BackupCodeMapper.php
@@ -0,0 +1,66 @@
+<?php
+
+/**
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\TwoFactor_BackupCodes\Db;
+
+use OCP\AppFramework\Db\Mapper;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IDb;
+use OCP\IUser;
+
+class BackupCodeMapper extends Mapper {
+
+ public function __construct(IDb $db) {
+ parent::__construct($db, 'twofactor_backup_codes');
+ }
+
+ /**
+ * @param IUser $user
+ * @return BackupCode[]
+ */
+ public function getBackupCodes(IUser $user) {
+ /* @var $qb IQueryBuilder */
+ $qb = $this->db->getQueryBuilder();
+
+ $qb->select('id', 'user_id', 'code', 'used')
+ ->from('twofactor_backup_codes')
+ ->where($qb->expr()->eq('user_id', $qb->createNamedParameter($user->getUID())));
+ $result = $qb->execute();
+
+ $rows = $result->fetchAll();
+ $result->closeCursor();
+
+ return array_map(function ($row) {
+ return BackupCode::fromRow($row);
+ }, $rows);
+ }
+
+ public function deleteCodes(IUser $user) {
+ /* @var $qb IQueryBuilder */
+ $qb = $this->db->getQueryBuilder();
+
+ $qb->delete('twofactor_backup_codes')
+ ->where($qb->expr()->eq('user_id', $qb->createNamedParameter($user->getUID())));
+ $qb->execute();
+ }
+
+}
diff --git a/apps/twofactor_backupcodes/lib/Provider/BackupCodesProvider.php b/apps/twofactor_backupcodes/lib/Provider/BackupCodesProvider.php
new file mode 100644
index 00000000000..91975dfad8d
--- /dev/null
+++ b/apps/twofactor_backupcodes/lib/Provider/BackupCodesProvider.php
@@ -0,0 +1,102 @@
+<?php
+
+/**
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\TwoFactor_BackupCodes\Provider;
+
+use OCA\TwoFactor_BackupCodes\Service\BackupCodeStorage;
+use OCP\Authentication\TwoFactorAuth\IProvider;
+use OCP\IL10N;
+use OCP\IUser;
+use OCP\Template;
+
+class BackupCodesProvider implements IProvider {
+
+ /** @var BackupCodeStorage */
+ private $storage;
+
+ /** @var IL10N */
+ private $l10n;
+
+ public function __construct(BackupCodeStorage $storage, IL10N $l10n) {
+ $this->l10n = $l10n;
+ $this->storage = $storage;
+ }
+
+ /**
+ * Get unique identifier of this 2FA provider
+ *
+ * @return string
+ */
+ public function getId() {
+ return 'backup_codes';
+ }
+
+ /**
+ * Get the display name for selecting the 2FA provider
+ *
+ * @return string
+ */
+ public function getDisplayName() {
+ return $this->l10n->t('Backup code');
+ }
+
+ /**
+ * Get the description for selecting the 2FA provider
+ *
+ * @return string
+ */
+ public function getDescription() {
+ return $this->l10n->t('Use backup code');
+ }
+
+ /**
+ * Get the template for rending the 2FA provider view
+ *
+ * @param IUser $user
+ * @return Template
+ */
+ public function getTemplate(IUser $user) {
+ $tmpl = new Template('twofactor_backupcodes', 'challenge');
+ return $tmpl;
+ }
+
+ /**
+ * Verify the given challenge
+ *
+ * @param IUser $user
+ * @param string $challenge
+ */
+ public function verifyChallenge(IUser $user, $challenge) {
+ return $this->storage->validateCode($user, $challenge);
+ }
+
+ /**
+ * Decides whether 2FA is enabled for the given user
+ *
+ * @param IUser $user
+ * @return boolean
+ */
+ public function isTwoFactorAuthEnabledForUser(IUser $user) {
+ return $this->storage->hasBackupCodes($user);
+ }
+
+}
diff --git a/apps/twofactor_backupcodes/lib/Service/BackupCodeStorage.php b/apps/twofactor_backupcodes/lib/Service/BackupCodeStorage.php
new file mode 100644
index 00000000000..9c78581255f
--- /dev/null
+++ b/apps/twofactor_backupcodes/lib/Service/BackupCodeStorage.php
@@ -0,0 +1,121 @@
+<?php
+
+/**
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\TwoFactor_BackupCodes\Service;
+
+use OCA\TwoFactor_BackupCodes\Db\BackupCode;
+use OCA\TwoFactor_BackupCodes\Db\BackupCodeMapper;
+use OCP\IUser;
+use OCP\Security\IHasher;
+use OCP\Security\ISecureRandom;
+
+class BackupCodeStorage {
+
+ /** @var BackupCodeMapper */
+ private $mapper;
+
+ /** @var IHasher */
+ private $hasher;
+
+ /** @var ISecureRandom */
+ private $random;
+
+ public function __construct(BackupCodeMapper $mapper, ISecureRandom $random, IHasher $hasher) {
+ $this->mapper = $mapper;
+ $this->hasher = $hasher;
+ $this->random = $random;
+ }
+
+ /**
+ * @param IUser $user
+ * @return string[]
+ */
+ public function createCodes(IUser $user, $number = 10) {
+ $result = [];
+
+ // Delete existing ones
+ $this->mapper->deleteCodes($user);
+
+ $uid = $user->getUID();
+ foreach (range(1, min([$number, 20])) as $i) {
+ $code = $this->random->generate(10, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789');
+
+ $dbCode = new BackupCode();
+ $dbCode->setUserId($uid);
+ $dbCode->setCode($this->hasher->hash($code));
+ $dbCode->setUsed(0);
+ $this->mapper->insert($dbCode);
+
+ array_push($result, $code);
+ }
+
+ return $result;
+ }
+
+ /**
+ * @param IUser $user
+ * @return bool
+ */
+ public function hasBackupCodes(IUser $user) {
+ $codes = $this->mapper->getBackupCodes($user);
+ return count($codes) > 0;
+ }
+
+ /**
+ * @param IUser $user
+ * @return array
+ */
+ public function getBackupCodesState(IUser $user) {
+ $codes = $this->mapper->getBackupCodes($user);
+ $total = count($codes);
+ $used = 0;
+ array_walk($codes, function (BackupCode $code) use (&$used) {
+ if (1 === (int) $code->getUsed()) {
+ $used++;
+ }
+ });
+ return [
+ 'enabled' => $total > 0,
+ 'total' => $total,
+ 'used' => $used,
+ ];
+ }
+
+ /**
+ * @param IUser $user
+ * @param string $code
+ * @return bool
+ */
+ public function validateCode(IUser $user, $code) {
+ $dbCodes = $this->mapper->getBackupCodes($user);
+
+ foreach ($dbCodes as $dbCode) {
+ if (0 === (int) $dbCode->getUsed() && $this->hasher->verify($code, $dbCode->getCode())) {
+ $dbCode->setUsed(1);
+ $this->mapper->update($dbCode);
+ return true;
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/apps/twofactor_backupcodes/settings/personal.php b/apps/twofactor_backupcodes/settings/personal.php
new file mode 100644
index 00000000000..037516e39a3
--- /dev/null
+++ b/apps/twofactor_backupcodes/settings/personal.php
@@ -0,0 +1,5 @@
+<?php
+
+$tmpl = new \OCP\Template('twofactor_backupcodes', 'personal');
+
+return $tmpl->fetchPage();
diff --git a/apps/twofactor_backupcodes/templates/challenge.php b/apps/twofactor_backupcodes/templates/challenge.php
new file mode 100644
index 00000000000..e6ec7bb52e6
--- /dev/null
+++ b/apps/twofactor_backupcodes/templates/challenge.php
@@ -0,0 +1,8 @@
+<?php
+style('twofactor_backupcodes', 'style');
+?>
+
+<form method="POST" class="challenge-form">
+ <input type="text" class="challenge" name="challenge" required="required" autofocus autocomplete="off" autocapitalize="off" placeholder="<?php p($l->t('Backup code')) ?>">
+ <input type="submit" class="confirm-inline icon-confirm" value="">
+</form>
diff --git a/apps/twofactor_backupcodes/templates/personal.php b/apps/twofactor_backupcodes/templates/personal.php
new file mode 100644
index 00000000000..23b06e23058
--- /dev/null
+++ b/apps/twofactor_backupcodes/templates/personal.php
@@ -0,0 +1,12 @@
+<?php
+
+script('twofactor_backupcodes', 'settingsview');
+script('twofactor_backupcodes', 'settings');
+style('twofactor_backupcodes', 'style');
+
+?>
+
+<div class="section">
+ <h2><?php p($l->t('Second-factor backup codes')); ?></h2>
+ <div id="twofactor-backupcodes-settings"></div>
+</div>
diff --git a/apps/twofactor_backupcodes/tests/Integration/Db/BackupCodeMapperTest.php b/apps/twofactor_backupcodes/tests/Integration/Db/BackupCodeMapperTest.php
new file mode 100644
index 00000000000..5d7d71dd17a
--- /dev/null
+++ b/apps/twofactor_backupcodes/tests/Integration/Db/BackupCodeMapperTest.php
@@ -0,0 +1,113 @@
+<?php
+
+/**
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\TwoFactor_BackupCodes\Tests\Integration\Db;
+
+use OC;
+use OCA\TwoFactor_BackupCodes\Db\BackupCode;
+use OCA\TwoFactor_BackupCodes\Db\BackupCodeMapper;
+use OCP\IDBConnection;
+use OCP\IUser;
+use Test\TestCase;
+
+/**
+ * @group DB
+ */
+class BackupCodeMapperTest extends TestCase {
+
+ /** @var IDBConnection */
+ private $db;
+
+ /** @var BackupCodeMapper */
+ private $mapper;
+
+ /** @var string */
+ private $testUID = 'test123456';
+
+ private function resetDB() {
+ $qb = $this->db->getQueryBuilder();
+ $qb->delete($this->mapper->getTableName())
+ ->where($qb->expr()->eq('user_id', $qb->createNamedParameter($this->testUID)));
+ $qb->execute();
+ }
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->db = OC::$server->getDatabaseConnection();
+ $this->mapper = OC::$server->query(BackupCodeMapper::class);
+
+ $this->resetDB();
+ }
+
+ protected function tearDown() {
+ parent::tearDown();
+
+ $this->resetDB();
+ }
+
+ public function testGetBackupCodes() {
+ $code1 = new BackupCode();
+ $code1->setUserId($this->testUID);
+ $code1->setCode('1|$2y$10$Fyo.DkMtkaHapVvRVbQBeeIdi5x/6nmPnxiBzD0GDKa08NMus5xze');
+ $code1->setUsed(1);
+
+ $code2 = new BackupCode();
+ $code2->setUserId($this->testUID);
+ $code2->setCode('1|$2y$10$nj3sZaCqGN8t6.SsnNADt.eX34UCkdX6FPx.r.rIwE6Jj3vi5wyt2');
+ $code2->setUsed(0);
+
+ $this->mapper->insert($code1);
+ $this->mapper->insert($code2);
+
+ $user = $this->getMockBuilder(IUser::class)->getMock();
+ $user->expects($this->once())
+ ->method('getUID')
+ ->will($this->returnValue($this->testUID));
+
+ $dbCodes = $this->mapper->getBackupCodes($user);
+
+ $this->assertCount(2, $dbCodes);
+ $this->assertInstanceOf(BackupCode::class, $dbCodes[0]);
+ $this->assertInstanceOf(BackupCode::class, $dbCodes[1]);
+ }
+
+ public function testDeleteCodes() {
+ $code = new BackupCode();
+ $code->setUserId($this->testUID);
+ $code->setCode('1|$2y$10$CagG8pEhZL.xDirtCCP/KuuWtnsAasgq60zY9rU46dBK4w8yW0Z/y');
+ $code->setUsed(1);
+ $user = $this->getMockBuilder(IUser::class)->getMock();
+ $user->expects($this->any())
+ ->method('getUID')
+ ->will($this->returnValue($this->testUID));
+
+ $this->mapper->insert($code);
+
+ $this->assertCount(1, $this->mapper->getBackupCodes($user));
+
+ $this->mapper->deleteCodes($user);
+
+ $this->assertCount(0, $this->mapper->getBackupCodes($user));
+ }
+
+}
diff --git a/apps/twofactor_backupcodes/tests/Integration/Service/BackupCodeStorageTest.php b/apps/twofactor_backupcodes/tests/Integration/Service/BackupCodeStorageTest.php
new file mode 100644
index 00000000000..5517af5ce0d
--- /dev/null
+++ b/apps/twofactor_backupcodes/tests/Integration/Service/BackupCodeStorageTest.php
@@ -0,0 +1,90 @@
+<?php
+
+/**
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\TwoFactor_BackupCodes\Tests\Integration\Service;
+
+use OC;
+use OCA\TwoFactor_BackupCodes\Service\BackupCodeStorage;
+use Test\TestCase;
+
+/**
+ * @group DB
+ */
+class BackupCodeStorageTest extends TestCase {
+
+ /** @var BackupCodeStorage */
+ private $storage;
+
+ /** @var string */
+ private $testUID = 'test123456789';
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->storage = OC::$server->query(BackupCodeStorage::class);
+ }
+
+ public function testSimpleWorkFlow() {
+ $user = $this->getMockBuilder(\OCP\IUser::class)->getMock();
+ $user->expects($this->any())
+ ->method('getUID')
+ ->will($this->returnValue($this->testUID));
+
+ // Create codes
+ $codes = $this->storage->createCodes($user, 5);
+ $this->assertCount(5, $codes);
+ $this->assertTrue($this->storage->hasBackupCodes($user));
+ $initialState = [
+ 'enabled' => true,
+ 'total' => 5,
+ 'used' => 0,
+ ];
+ $this->assertEquals($initialState, $this->storage->getBackupCodesState($user));
+
+ // Use codes
+ $code = $codes[2];
+ $this->assertTrue($this->storage->validateCode($user, $code));
+ // Code must not be used twice
+ $this->assertFalse($this->storage->validateCode($user, $code));
+ // Invalid codes are invalid
+ $this->assertFalse($this->storage->validateCode($user, 'I DO NOT EXIST'));
+ $stateAfter = [
+ 'enabled' => true,
+ 'total' => 5,
+ 'used' => 1,
+ ];
+ $this->assertEquals($stateAfter, $this->storage->getBackupCodesState($user));
+
+ // Deplete codes
+ $this->assertTrue($this->storage->validateCode($user, $codes[0]));
+ $this->assertTrue($this->storage->validateCode($user, $codes[1]));
+ $this->assertTrue($this->storage->validateCode($user, $codes[3]));
+ $this->assertTrue($this->storage->validateCode($user, $codes[4]));
+ $stateAllUsed = [
+ 'enabled' => true,
+ 'total' => 5,
+ 'used' => 5,
+ ];
+ $this->assertEquals($stateAllUsed, $this->storage->getBackupCodesState($user));
+ }
+
+}
diff --git a/apps/twofactor_backupcodes/tests/Unit/Controller/SettingsControllerTest.php b/apps/twofactor_backupcodes/tests/Unit/Controller/SettingsControllerTest.php
new file mode 100644
index 00000000000..918d1a8c64d
--- /dev/null
+++ b/apps/twofactor_backupcodes/tests/Unit/Controller/SettingsControllerTest.php
@@ -0,0 +1,95 @@
+<?php
+
+/**
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\TwoFactor_BackupCodes\Tests\Unit\Controller;
+
+use OCA\TwoFactor_BackupCodes\Controller\SettingsController;
+use OCA\TwoFactor_BackupCodes\Service\BackupCodeStorage;
+use OCP\IRequest;
+use OCP\IUser;
+use OCP\IUserSession;
+use Test\TestCase;
+
+class SettingsControllerTest extends TestCase {
+
+ /** @var IRequest|PHPUnit_Framework_MockObject_MockObject */
+ private $request;
+
+ /** @var BackupCodeStorage|PHPUnit_Framework_MockObject_MockObject */
+ private $storage;
+
+ /** @var IUserSession|PHPUnit_Framework_MockObject_MockObject */
+ private $userSession;
+
+ /** @var SettingsController */
+ private $controller;
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->request = $this->getMockBuilder(IRequest::class)->getMock();
+ $this->storage = $this->getMockBuilder(BackupCodeStorage::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->userSession = $this->getMockBuilder(IUserSession::class)->getMock();
+
+ $this->controller = new SettingsController('twofactor_backupcodes', $this->request, $this->storage, $this->userSession);
+ }
+
+ public function testState() {
+ $user = $this->getMockBuilder(IUser::class)->getMock();
+
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->will($this->returnValue($user));
+ $this->storage->expects($this->once())
+ ->method('getBackupCodesState')
+ ->with($user)
+ ->will($this->returnValue('state'));
+
+ $this->assertEquals('state', $this->controller->state());
+ }
+
+ public function testCreateCodes() {
+ $user = $this->getMockBuilder(IUser::class)->getMock();
+
+ $codes = ['a', 'b'];
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->will($this->returnValue($user));
+ $this->storage->expects($this->once())
+ ->method('createCodes')
+ ->with($user)
+ ->will($this->returnValue($codes));
+ $this->storage->expects($this->once())
+ ->method('getBackupCodesState')
+ ->with($user)
+ ->will($this->returnValue('state'));
+
+ $expected = [
+ 'codes' => $codes,
+ 'state' => 'state',
+ ];
+ $this->assertEquals($expected, $this->controller->createCodes());
+ }
+
+}
diff --git a/apps/twofactor_backupcodes/tests/Unit/Provider/BackupCodesProviderTest.php b/apps/twofactor_backupcodes/tests/Unit/Provider/BackupCodesProviderTest.php
new file mode 100644
index 00000000000..a744a44e609
--- /dev/null
+++ b/apps/twofactor_backupcodes/tests/Unit/Provider/BackupCodesProviderTest.php
@@ -0,0 +1,103 @@
+<?php
+
+/**
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\TwoFactor_BackupCodes\Tests\Unit\Provider;
+
+use OCA\TwoFactor_BackupCodes\Provider\BackupCodesProvider;
+use OCA\TwoFactor_BackupCodes\Service\BackupCodeStorage;
+use OCP\IL10N;
+use OCP\IUser;
+use OCP\Template;
+use Test\TestCase;
+
+class BackupCodesProviderTest extends TestCase {
+
+ /** @var BackupCodeStorage|PHPUnit_Framework_MockObject_MockObject */
+ private $storage;
+
+ /** @var IL10N|PHPUnit_Framework_MockObject_MockObject */
+ private $l10n;
+
+ /** @var BackupCodesProvider */
+ private $provider;
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->storage = $this->getMockBuilder(BackupCodeStorage::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->l10n = $this->getMockBuilder(IL10N::class)->getMock();
+ $this->provider = new BackupCodesProvider($this->storage, $this->l10n);
+ }
+
+ public function testGetId() {
+ $this->assertEquals('backup_codes', $this->provider->getId());
+ }
+
+ public function testGetDisplayName() {
+ $this->l10n->expects($this->once())
+ ->method('t')
+ ->with('Backup code')
+ ->will($this->returnValue('l10n backup code'));
+ $this->assertSame('l10n backup code', $this->provider->getDisplayName());
+ }
+
+ public function testGetDescription() {
+ $this->l10n->expects($this->once())
+ ->method('t')
+ ->with('Use backup code')
+ ->will($this->returnValue('l10n use backup code'));
+ $this->assertSame('l10n use backup code', $this->provider->getDescription());
+ }
+
+ public function testGetTempalte() {
+ $user = $this->getMockBuilder(IUser::class)->getMock();
+ $expected = new Template('twofactor_backupcodes', 'challenge');
+
+ $this->assertEquals($expected, $this->provider->getTemplate($user));
+ }
+
+ public function testVerfiyChallenge() {
+ $user = $this->getMockBuilder(IUser::class)->getMock();
+ $challenge = 'xyz';
+
+ $this->storage->expects($this->once())
+ ->method('validateCode')
+ ->with($user, $challenge)
+ ->will($this->returnValue(false));
+
+ $this->assertFalse($this->provider->verifyChallenge($user, $challenge));
+ }
+
+ public function testIsTwoFactorEnabledForUser() {
+ $user = $this->getMockBuilder(IUser::class)->getMock();
+
+ $this->storage->expects($this->once())
+ ->method('hasBackupCodes')
+ ->with($user)
+ ->will($this->returnValue(true));
+
+ $this->assertTrue($this->provider->isTwoFactorAuthEnabledForUser($user));
+ }
+
+}
diff --git a/apps/twofactor_backupcodes/tests/Unit/Service/BackupCodeStorageTest.php b/apps/twofactor_backupcodes/tests/Unit/Service/BackupCodeStorageTest.php
new file mode 100644
index 00000000000..04c51fa7e14
--- /dev/null
+++ b/apps/twofactor_backupcodes/tests/Unit/Service/BackupCodeStorageTest.php
@@ -0,0 +1,228 @@
+<?php
+
+/**
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\TwoFactor_BackupCodes\Tests\Unit\Service;
+
+use OCA\TwoFactor_BackupCodes\Db\BackupCode;
+use OCA\TwoFactor_BackupCodes\Db\BackupCodeMapper;
+use OCA\TwoFactor_BackupCodes\Service\BackupCodeStorage;
+use OCP\IUser;
+use OCP\Security\IHasher;
+use OCP\Security\ISecureRandom;
+use Test\TestCase;
+
+class BackupCodeStorageTest extends TestCase {
+
+ /** @var BackupCodeMapper|PHPUnit_Framework_MockObject_MockObject */
+ private $mapper;
+
+ /** @var ISecureRandom|PHPUnit_Framework_MockObject_MockObject */
+ private $random;
+
+ /** @var IHasher|PHPUnit_Framework_MockObject_MockObject */
+ private $hasher;
+
+ /** @var BackupCodeStorage */
+ private $storage;
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->mapper = $this->getMockBuilder(BackupCodeMapper::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->random = $this->getMockBuilder(ISecureRandom::class)->getMock();
+ $this->hasher = $this->getMockBuilder(IHasher::class)->getMock();
+ $this->storage = new BackupCodeStorage($this->mapper, $this->random, $this->hasher);
+ }
+
+ public function testCreateCodes() {
+ $user = $this->getMockBuilder(IUser::class)->getMock();
+ $number = 5;
+
+ $user->expects($this->once())
+ ->method('getUID')
+ ->will($this->returnValue('fritz'));
+ $this->random->expects($this->exactly($number))
+ ->method('generate')
+ ->with(10, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
+ ->will($this->returnValue('CODEABCDEF'));
+ $this->hasher->expects($this->exactly($number))
+ ->method('hash')
+ ->with('CODEABCDEF')
+ ->will($this->returnValue('HASHEDCODE'));
+ $row = new BackupCode();
+ $row->setUserId('fritz');
+ $row->setCode('HASHEDCODE');
+ $row->setUsed(0);
+ $this->mapper->expects($this->exactly($number))
+ ->method('insert')
+ ->with($this->equalTo($row));
+
+ $codes = $this->storage->createCodes($user, $number);
+ $this->assertCount($number, $codes);
+ foreach ($codes as $code) {
+ $this->assertEquals('CODEABCDEF', $code);
+ }
+ }
+
+ public function testHasBackupCodes() {
+ $user = $this->getMockBuilder(IUser::class)->getMock();
+ $codes = [
+ new BackupCode(),
+ new BackupCode(),
+ ];
+
+ $this->mapper->expects($this->once())
+ ->method('getBackupCodes')
+ ->with($user)
+ ->will($this->returnValue($codes));
+
+ $this->assertTrue($this->storage->hasBackupCodes($user));
+ }
+
+ public function testHasBackupCodesNoCodes() {
+ $user = $this->getMockBuilder(IUser::class)->getMock();
+ $codes = [];
+
+ $this->mapper->expects($this->once())
+ ->method('getBackupCodes')
+ ->with($user)
+ ->will($this->returnValue($codes));
+
+ $this->assertFalse($this->storage->hasBackupCodes($user));
+ }
+
+ public function testGetBackupCodeState() {
+ $user = $this->getMockBuilder(IUser::class)->getMock();
+
+ $code1 = new BackupCode();
+ $code1->setUsed(1);
+ $code2 = new BackupCode();
+ $code2->setUsed('0');
+ $codes = [
+ $code1,
+ $code2,
+ ];
+
+ $this->mapper->expects($this->once())
+ ->method('getBackupCodes')
+ ->with($user)
+ ->will($this->returnValue($codes));
+
+ $expected = [
+ 'enabled' => true,
+ 'total' => 2,
+ 'used' => 1,
+ ];
+ $this->assertEquals($expected, $this->storage->getBackupCodesState($user));
+ }
+
+ public function testGetBackupCodeDisabled() {
+ $user = $this->getMockBuilder(IUser::class)->getMock();
+
+ $codes = [];
+
+ $this->mapper->expects($this->once())
+ ->method('getBackupCodes')
+ ->with($user)
+ ->will($this->returnValue($codes));
+
+ $expected = [
+ 'enabled' => false,
+ 'total' => 0,
+ 'used' => 0,
+ ];
+ $this->assertEquals($expected, $this->storage->getBackupCodesState($user));
+ }
+
+ public function testValidateCode() {
+ $user = $this->getMockBuilder(IUser::class)->getMock();
+ $code = new BackupCode();
+ $code->setUsed(0);
+ $code->setCode('HASHEDVALUE');
+ $codes = [
+ $code,
+ ];
+
+ $this->mapper->expects($this->once())
+ ->method('getBackupCodes')
+ ->with($user)
+ ->will($this->returnValue($codes));
+ $this->hasher->expects($this->once())
+ ->method('verify')
+ ->with('CHALLENGE', 'HASHEDVALUE')
+ ->will($this->returnValue(true));
+ $this->mapper->expects($this->once())
+ ->method('update')
+ ->with($code);
+
+ $this->assertTrue($this->storage->validateCode($user, 'CHALLENGE'));
+
+ $this->assertEquals(1, $code->getUsed());
+ }
+
+ public function testValidateUsedCode() {
+ $user = $this->getMockBuilder(IUser::class)->getMock();
+ $code = new BackupCode();
+ $code->setUsed('1');
+ $code->setCode('HASHEDVALUE');
+ $codes = [
+ $code,
+ ];
+
+ $this->mapper->expects($this->once())
+ ->method('getBackupCodes')
+ ->with($user)
+ ->will($this->returnValue($codes));
+ $this->hasher->expects($this->never())
+ ->method('verifiy');
+ $this->mapper->expects($this->never())
+ ->method('update');
+
+ $this->assertFalse($this->storage->validateCode($user, 'CHALLENGE'));
+ }
+
+ public function testValidateCodeWithWrongHash() {
+ $user = $this->getMockBuilder(IUser::class)->getMock();
+ $code = new BackupCode();
+ $code->setUsed(0);
+ $code->setCode('HASHEDVALUE');
+ $codes = [
+ $code,
+ ];
+
+ $this->mapper->expects($this->once())
+ ->method('getBackupCodes')
+ ->with($user)
+ ->will($this->returnValue($codes));
+ $this->hasher->expects($this->once())
+ ->method('verify')
+ ->with('CHALLENGE', 'HASHEDVALUE')
+ ->will($this->returnValue(false));
+ $this->mapper->expects($this->never())
+ ->method('update');
+
+ $this->assertFalse($this->storage->validateCode($user, 'CHALLENGE'));
+ }
+
+}
diff --git a/core/Controller/TwoFactorChallengeController.php b/core/Controller/TwoFactorChallengeController.php
index 48dea062812..b2614138123 100644
--- a/core/Controller/TwoFactorChallengeController.php
+++ b/core/Controller/TwoFactorChallengeController.php
@@ -82,9 +82,11 @@ class TwoFactorChallengeController extends Controller {
public function selectChallenge($redirect_url) {
$user = $this->userSession->getUser();
$providers = $this->twoFactorManager->getProviders($user);
+ $backupProvider = $this->twoFactorManager->getBackupProvider($user);
$data = [
'providers' => $providers,
+ 'backupProvider' => $backupProvider,
'redirect_url' => $redirect_url,
'logout_attribute' => $this->getLogoutAttribute(),
];
@@ -107,6 +109,12 @@ class TwoFactorChallengeController extends Controller {
return new RedirectResponse($this->urlGenerator->linkToRoute('core.TwoFactorChallenge.selectChallenge'));
}
+ $backupProvider = $this->twoFactorManager->getBackupProvider($user);
+ if (!is_null($backupProvider) && $backupProvider->getId() === $provider->getId()) {
+ // Don't show the backup provider link if we're already showing that provider's challenge
+ $backupProvider = null;
+ }
+
if ($this->session->exists('two_factor_auth_error')) {
$this->session->remove('two_factor_auth_error');
$error = true;
@@ -118,6 +126,7 @@ class TwoFactorChallengeController extends Controller {
$data = [
'error' => $error,
'provider' => $provider,
+ 'backupProvider' => $backupProvider,
'logout_attribute' => $this->getLogoutAttribute(),
'template' => $tmpl->fetchPage(),
];
diff --git a/core/css/styles.css b/core/css/styles.css
index 25bc2d086d5..efc49e02a17 100644
--- a/core/css/styles.css
+++ b/core/css/styles.css
@@ -45,7 +45,7 @@ body {
border: none !important;
}
-.two-factor-cancel {
+.two-factor-link {
display: inline-block;
padding: 12px;
color: rgba(255, 255, 255, .75);
diff --git a/core/shipped.json b/core/shipped.json
index f944d9d4c11..0ddaf68eb64 100644
--- a/core/shipped.json
+++ b/core/shipped.json
@@ -28,6 +28,7 @@
"survey_client",
"systemtags",
"templateeditor",
+ "twofactor_backupcodes",
"theming",
"updatenotification",
"user_external",
@@ -39,6 +40,7 @@
"files",
"dav",
"federatedfilesharing",
+ "twofactor_backupcodes",
"workflowengine"
]
}
diff --git a/core/templates/twofactorselectchallenge.php b/core/templates/twofactorselectchallenge.php
index 1948499e604..5cfba8bcb47 100644
--- a/core/templates/twofactorselectchallenge.php
+++ b/core/templates/twofactorselectchallenge.php
@@ -19,4 +19,12 @@
</ul>
</p>
</div>
-<a class="two-factor-cancel" <?php print_unescaped($_['logout_attribute']); ?>><?php p($l->t('Cancel log in')) ?></a>
+<a class="two-factor-link" <?php print_unescaped($_['logout_attribute']); ?>><?php p($l->t('Cancel log in')) ?></a>
+<?php if (!is_null($_['backupProvider'])): ?>
+<a class="two-factor-link" href="<?php p(\OC::$server->getURLGenerator()->linkToRoute('core.TwoFactorChallenge.showChallenge',
+ [
+ 'challengeProviderId' => $_['backupProvider']->getId(),
+ 'redirect_url' => $_['redirect_url'],
+ ]
+ )) ?>"><?php p($l->t('Use backup code')) ?></a>
+<?php endif;
diff --git a/core/templates/twofactorshowchallenge.php b/core/templates/twofactorshowchallenge.php
index fb161921675..197de8a288e 100644
--- a/core/templates/twofactorshowchallenge.php
+++ b/core/templates/twofactorshowchallenge.php
@@ -16,4 +16,12 @@ $template = $_['template'];
<?php endif; ?>
<?php print_unescaped($template); ?>
</div>
-<a class="two-factor-cancel" <?php print_unescaped($_['logout_attribute']); ?>><?php p($l->t('Cancel log in')) ?></a>
+<a class="two-factor-link" <?php print_unescaped($_['logout_attribute']); ?>><?php p($l->t('Cancel log in')) ?></a>
+<?php if (!is_null($_['backupProvider'])): ?>
+<a class="two-factor-link" href="<?php p(\OC::$server->getURLGenerator()->linkToRoute('core.TwoFactorChallenge.showChallenge',
+ [
+ 'challengeProviderId' => $_['backupProvider']->getId(),
+ 'redirect_url' => $_['redirect_url'],
+ ]
+ )) ?>"><?php p($l->t('Use backup code')) ?></a>
+<?php endif;
diff --git a/lib/private/Authentication/TwoFactorAuth/Manager.php b/lib/private/Authentication/TwoFactorAuth/Manager.php
index 143fe7dc927..1bea7aa3478 100644
--- a/lib/private/Authentication/TwoFactorAuth/Manager.php
+++ b/lib/private/Authentication/TwoFactorAuth/Manager.php
@@ -35,6 +35,8 @@ use OCP\IUser;
class Manager {
const SESSION_UID_KEY = 'two_factor_auth_uid';
+ const BACKUP_CODES_APP_ID = 'twofactor_backupcodes';
+ const BACKUP_CODES_PROVIDER_ID = 'backup_codes';
/** @var AppManager */
private $appManager;
@@ -93,21 +95,35 @@ class Manager {
* @return IProvider|null
*/
public function getProvider(IUser $user, $challengeProviderId) {
- $providers = $this->getProviders($user);
+ $providers = $this->getProviders($user, true);
return isset($providers[$challengeProviderId]) ? $providers[$challengeProviderId] : null;
}
/**
+ * @param IUser $user
+ * @return IProvider|null the backup provider, if enabled for the given user
+ */
+ public function getBackupProvider(IUser $user) {
+ $providers = $this->getProviders($user, true);
+ return $providers[self::BACKUP_CODES_PROVIDER_ID];
+ }
+
+ /**
* Get the list of 2FA providers for the given user
*
* @param IUser $user
+ * @param bool $includeBackupApp
* @return IProvider[]
*/
- public function getProviders(IUser $user) {
+ public function getProviders(IUser $user, $includeBackupApp = false) {
$allApps = $this->appManager->getEnabledAppsForUser($user);
$providers = [];
foreach ($allApps as $appId) {
+ if (!$includeBackupApp && $appId === self::BACKUP_CODES_APP_ID) {
+ continue;
+ }
+
$info = $this->appManager->getAppInfo($appId);
if (isset($info['two-factor-providers'])) {
$providerClasses = $info['two-factor-providers'];
diff --git a/tests/Core/Controller/TwoFactorChallengeControllerTest.php b/tests/Core/Controller/TwoFactorChallengeControllerTest.php
index 28f16241e2f..8a1cfb9edf1 100644
--- a/tests/Core/Controller/TwoFactorChallengeControllerTest.php
+++ b/tests/Core/Controller/TwoFactorChallengeControllerTest.php
@@ -77,9 +77,14 @@ class TwoFactorChallengeControllerTest extends TestCase {
->method('getProviders')
->with($user)
->will($this->returnValue($providers));
+ $this->twoFactorManager->expects($this->once())
+ ->method('getBackupProvider')
+ ->with($user)
+ ->will($this->returnValue('backup'));
$expected = new \OCP\AppFramework\Http\TemplateResponse('core', 'twofactorselectchallenge', [
'providers' => $providers,
+ 'backupProvider' => 'backup',
'redirect_url' => '/some/url',
'logout_attribute' => 'logoutAttribute',
], 'guest');
@@ -92,6 +97,9 @@ class TwoFactorChallengeControllerTest extends TestCase {
$provider = $this->getMockBuilder('\OCP\Authentication\TwoFactorAuth\IProvider')
->disableOriginalConstructor()
->getMock();
+ $backupProvider = $this->getMockBuilder('\OCP\Authentication\TwoFactorAuth\IProvider')
+ ->disableOriginalConstructor()
+ ->getMock();
$tmpl = $this->getMockBuilder('\OCP\Template')
->disableOriginalConstructor()
->getMock();
@@ -103,6 +111,16 @@ class TwoFactorChallengeControllerTest extends TestCase {
->method('getProvider')
->with($user, 'myprovider')
->will($this->returnValue($provider));
+ $this->twoFactorManager->expects($this->once())
+ ->method('getBackupProvider')
+ ->with($user)
+ ->will($this->returnValue($backupProvider));
+ $provider->expects($this->once())
+ ->method('getId')
+ ->will($this->returnValue('u2f'));
+ $backupProvider->expects($this->once())
+ ->method('getId')
+ ->will($this->returnValue('backup_codes'));
$this->session->expects($this->once())
->method('exists')
@@ -122,6 +140,7 @@ class TwoFactorChallengeControllerTest extends TestCase {
$expected = new \OCP\AppFramework\Http\TemplateResponse('core', 'twofactorshowchallenge', [
'error' => true,
'provider' => $provider,
+ 'backupProvider' => $backupProvider,
'logout_attribute' => 'logoutAttribute',
'template' => '<html/>',
], 'guest');
diff --git a/tests/lib/Authentication/TwoFactorAuth/ManagerTest.php b/tests/lib/Authentication/TwoFactorAuth/ManagerTest.php
index f9489150e21..83a74f2f253 100644
--- a/tests/lib/Authentication/TwoFactorAuth/ManagerTest.php
+++ b/tests/lib/Authentication/TwoFactorAuth/ManagerTest.php
@@ -22,54 +22,76 @@
namespace Test\Authentication\TwoFactorAuth;
-use Test\TestCase;
+use Exception;
+use OC;
+use OC\App\AppManager;
use OC\Authentication\TwoFactorAuth\Manager;
+use OCA\TwoFactor_BackupCodes\Provider\BackupCodesProvider;
+use OCP\Authentication\TwoFactorAuth\IProvider;
+use OCP\IConfig;
+use OCP\ISession;
+use OCP\IUser;
+use Test\TestCase;
class ManagerTest extends TestCase {
- /** @var \OCP\IUser|\PHPUnit_Framework_MockObject_MockObject */
+ /** @var IUser|PHPUnit_Framework_MockObject_MockObject */
private $user;
- /** @var \OC\App\AppManager|\PHPUnit_Framework_MockObject_MockObject */
+ /** @var AppManager|PHPUnit_Framework_MockObject_MockObject */
private $appManager;
- /** @var \OCP\ISession|\PHPUnit_Framework_MockObject_MockObject */
+ /** @var ISession|PHPUnit_Framework_MockObject_MockObject */
private $session;
/** @var Manager */
private $manager;
- /** @var \OCP\IConfig|\PHPUnit_Framework_MockObject_MockObject */
+ /** @var IConfig|PHPUnit_Framework_MockObject_MockObject */
private $config;
- /** @var \OCP\Authentication\TwoFactorAuth\IProvider|\PHPUnit_Framework_MockObject_MockObject */
+ /** @var IProvider|PHPUnit_Framework_MockObject_MockObject */
private $fakeProvider;
+ /** @var IProvider|PHPUnit_Framework_MockObject_MockObject */
+ private $backupProvider;
+
protected function setUp() {
parent::setUp();
- $this->user = $this->getMock('\OCP\IUser');
+ $this->user = $this->getMockBuilder('\OCP\IUser')->getMock();
$this->appManager = $this->getMockBuilder('\OC\App\AppManager')
->disableOriginalConstructor()
->getMock();
- $this->session = $this->getMock('\OCP\ISession');
- $this->config = $this->getMock('\OCP\IConfig');
+ $this->session = $this->getMockBuilder('\OCP\ISession')->getMock();
+ $this->config = $this->getMockBuilder('\OCP\IConfig')->getMock();
$this->manager = $this->getMockBuilder('\OC\Authentication\TwoFactorAuth\Manager')
->setConstructorArgs([$this->appManager, $this->session, $this->config])
->setMethods(['loadTwoFactorApp']) // Do not actually load the apps
->getMock();
- $this->fakeProvider = $this->getMock('\OCP\Authentication\TwoFactorAuth\IProvider');
+ $this->fakeProvider = $this->getMockBuilder('\OCP\Authentication\TwoFactorAuth\IProvider')->getMock();
$this->fakeProvider->expects($this->any())
->method('getId')
->will($this->returnValue('email'));
$this->fakeProvider->expects($this->any())
->method('isTwoFactorAuthEnabledForUser')
->will($this->returnValue(true));
- \OC::$server->registerService('\OCA\MyCustom2faApp\FakeProvider', function() {
+ OC::$server->registerService('\OCA\MyCustom2faApp\FakeProvider', function() {
return $this->fakeProvider;
});
+
+ $this->backupProvider = $this->getMockBuilder('\OCP\Authentication\TwoFactorAuth\IProvider')->getMock();
+ $this->backupProvider->expects($this->any())
+ ->method('getId')
+ ->will($this->returnValue('backup_codes'));
+ $this->backupProvider->expects($this->any())
+ ->method('isTwoFactorAuthEnabledForUser')
+ ->will($this->returnValue(true));
+ OC::$server->registerService('\OCA\TwoFactor_BackupCodes\Provider\FakeBackupCodesProvider', function () {
+ return $this->backupProvider;
+ });
}
private function prepareNoProviders() {
@@ -105,8 +127,40 @@ class ManagerTest extends TestCase {
->with('mycustom2faapp');
}
+ private function prepareProvidersWitBackupProvider() {
+ $this->appManager->expects($this->any())
+ ->method('getEnabledAppsForUser')
+ ->with($this->user)
+ ->will($this->returnValue([
+ 'mycustom2faapp',
+ 'twofactor_backupcodes',
+ ]));
+
+ $this->appManager->expects($this->exactly(2))
+ ->method('getAppInfo')
+ ->will($this->returnValueMap([
+ [
+ 'mycustom2faapp',
+ ['two-factor-providers' => [
+ '\OCA\MyCustom2faApp\FakeProvider',
+ ]
+ ]
+ ],
+ [
+ 'twofactor_backupcodes',
+ ['two-factor-providers' => [
+ '\OCA\TwoFactor_BackupCodes\Provider\FakeBackupCodesProvider',
+ ]
+ ]
+ ],
+ ]));
+
+ $this->manager->expects($this->exactly(2))
+ ->method('loadTwoFactorApp');
+ }
+
/**
- * @expectedException \Exception
+ * @expectedException Exception
* @expectedExceptionMessage Could not load two-factor auth provider \OCA\MyFaulty2faApp\DoesNotExist
*/
public function testFailHardIfProviderCanNotBeLoaded() {
@@ -150,6 +204,12 @@ class ManagerTest extends TestCase {
$this->assertSame($this->fakeProvider, $this->manager->getProvider($this->user, 'email'));
}
+ public function testGetBackupProvider() {
+ $this->prepareProvidersWitBackupProvider();
+
+ $this->assertSame($this->backupProvider, $this->manager->getBackupProvider($this->user));
+ }
+
public function testGetInvalidProvider() {
$this->prepareProviders();