summaryrefslogtreecommitdiffstats
path: root/apps
diff options
context:
space:
mode:
authorChristoph Wurst <christoph@winzerhof-wurst.at>2016-08-29 19:19:44 +0200
committerRoeland Jago Douma <roeland@famdouma.nl>2016-09-05 08:51:13 +0200
commit8acb734854484e2ffd235929f6e7d0ba4c273844 (patch)
tree3269bc6cc60b51d4fd507d91e8eca3a4ecc262cd /apps
parent8b484eedf029b8e1a9dcef0efb09db381888c4b0 (diff)
downloadnextcloud-server-8acb734854484e2ffd235929f6e7d0ba4c273844.tar.gz
nextcloud-server-8acb734854484e2ffd235929f6e7d0ba4c273844.zip
add 2fa backup codes app
* add backup codes app unit tests * add integration tests for the backup codes app
Diffstat (limited to 'apps')
-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
20 files changed, 1347 insertions, 0 deletions
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'));
+ }
+
+}