Merge pull request #1171 from nextcloud/2fa-backup-codes

add 2fa backup codes app
This commit is contained in:
Marius Blüm 2016-09-05 12:17:29 +02:00 committed by GitHub
commit f8eb7be7b1
33 changed files with 1510 additions and 22 deletions

1
.gitignore vendored
View File

@ -27,6 +27,7 @@
!/apps/admin_audit !/apps/admin_audit
!/apps/updatenotification !/apps/updatenotification
!/apps/theming !/apps/theming
!/apps/twofactor_backupcodes
!/apps/workflowengine !/apps/workflowengine
/apps/files_external/3rdparty/irodsphp/PHPUnitTest /apps/files_external/3rdparty/irodsphp/PHPUnitTest
/apps/files_external/3rdparty/irodsphp/web /apps/files_external/3rdparty/irodsphp/web

View File

@ -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');

View File

@ -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>

View File

@ -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>

View File

@ -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'
],
]
];

View File

@ -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;
}

View File

@ -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);

View File

@ -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, $, _);

View File

@ -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),
];
}
}

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,5 @@
<?php
$tmpl = new \OCP\Template('twofactor_backupcodes', 'personal');
return $tmpl->fetchPage();

View File

@ -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>

View File

@ -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>

View File

@ -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));
}
}

View File

@ -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));
}
}

View File

@ -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());
}
}

View File

@ -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));
}
}

View File

@ -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'));
}
}

View File

@ -293,6 +293,7 @@ Feature: provisioning
| provisioning_api | | provisioning_api |
| systemtags | | systemtags |
| theming | | theming |
| twofactor_backupcodes |
| updatenotification | | updatenotification |
| workflowengine | | workflowengine |
| files_external | | files_external |

View File

@ -82,9 +82,11 @@ class TwoFactorChallengeController extends Controller {
public function selectChallenge($redirect_url) { public function selectChallenge($redirect_url) {
$user = $this->userSession->getUser(); $user = $this->userSession->getUser();
$providers = $this->twoFactorManager->getProviders($user); $providers = $this->twoFactorManager->getProviders($user);
$backupProvider = $this->twoFactorManager->getBackupProvider($user);
$data = [ $data = [
'providers' => $providers, 'providers' => $providers,
'backupProvider' => $backupProvider,
'redirect_url' => $redirect_url, 'redirect_url' => $redirect_url,
'logout_attribute' => $this->getLogoutAttribute(), 'logout_attribute' => $this->getLogoutAttribute(),
]; ];
@ -107,6 +109,12 @@ class TwoFactorChallengeController extends Controller {
return new RedirectResponse($this->urlGenerator->linkToRoute('core.TwoFactorChallenge.selectChallenge')); 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')) { if ($this->session->exists('two_factor_auth_error')) {
$this->session->remove('two_factor_auth_error'); $this->session->remove('two_factor_auth_error');
$error = true; $error = true;
@ -118,6 +126,7 @@ class TwoFactorChallengeController extends Controller {
$data = [ $data = [
'error' => $error, 'error' => $error,
'provider' => $provider, 'provider' => $provider,
'backupProvider' => $backupProvider,
'logout_attribute' => $this->getLogoutAttribute(), 'logout_attribute' => $this->getLogoutAttribute(),
'template' => $tmpl->fetchPage(), 'template' => $tmpl->fetchPage(),
]; ];

View File

@ -45,7 +45,7 @@ body {
border: none !important; border: none !important;
} }
.two-factor-cancel { .two-factor-link {
display: inline-block; display: inline-block;
padding: 12px; padding: 12px;
color: rgba(255, 255, 255, .75); color: rgba(255, 255, 255, .75);

View File

@ -28,6 +28,7 @@
"survey_client", "survey_client",
"systemtags", "systemtags",
"templateeditor", "templateeditor",
"twofactor_backupcodes",
"theming", "theming",
"updatenotification", "updatenotification",
"user_external", "user_external",
@ -39,6 +40,7 @@
"files", "files",
"dav", "dav",
"federatedfilesharing", "federatedfilesharing",
"twofactor_backupcodes",
"workflowengine" "workflowengine"
] ]
} }

View File

@ -19,4 +19,12 @@
</ul> </ul>
</p> </p>
</div> </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;

View File

@ -16,4 +16,12 @@ $template = $_['template'];
<?php endif; ?> <?php endif; ?>
<?php print_unescaped($template); ?> <?php print_unescaped($template); ?>
</div> </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;

View File

@ -35,6 +35,8 @@ use OCP\IUser;
class Manager { class Manager {
const SESSION_UID_KEY = 'two_factor_auth_uid'; const SESSION_UID_KEY = 'two_factor_auth_uid';
const BACKUP_CODES_APP_ID = 'twofactor_backupcodes';
const BACKUP_CODES_PROVIDER_ID = 'backup_codes';
/** @var AppManager */ /** @var AppManager */
private $appManager; private $appManager;
@ -93,21 +95,35 @@ class Manager {
* @return IProvider|null * @return IProvider|null
*/ */
public function getProvider(IUser $user, $challengeProviderId) { public function getProvider(IUser $user, $challengeProviderId) {
$providers = $this->getProviders($user); $providers = $this->getProviders($user, true);
return isset($providers[$challengeProviderId]) ? $providers[$challengeProviderId] : null; 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 * Get the list of 2FA providers for the given user
* *
* @param IUser $user * @param IUser $user
* @param bool $includeBackupApp
* @return IProvider[] * @return IProvider[]
*/ */
public function getProviders(IUser $user) { public function getProviders(IUser $user, $includeBackupApp = false) {
$allApps = $this->appManager->getEnabledAppsForUser($user); $allApps = $this->appManager->getEnabledAppsForUser($user);
$providers = []; $providers = [];
foreach ($allApps as $appId) { foreach ($allApps as $appId) {
if (!$includeBackupApp && $appId === self::BACKUP_CODES_APP_ID) {
continue;
}
$info = $this->appManager->getAppInfo($appId); $info = $this->appManager->getAppInfo($appId);
if (isset($info['two-factor-providers'])) { if (isset($info['two-factor-providers'])) {
$providerClasses = $info['two-factor-providers']; $providerClasses = $info['two-factor-providers'];

View File

@ -77,9 +77,14 @@ class TwoFactorChallengeControllerTest extends TestCase {
->method('getProviders') ->method('getProviders')
->with($user) ->with($user)
->will($this->returnValue($providers)); ->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', [ $expected = new \OCP\AppFramework\Http\TemplateResponse('core', 'twofactorselectchallenge', [
'providers' => $providers, 'providers' => $providers,
'backupProvider' => 'backup',
'redirect_url' => '/some/url', 'redirect_url' => '/some/url',
'logout_attribute' => 'logoutAttribute', 'logout_attribute' => 'logoutAttribute',
], 'guest'); ], 'guest');
@ -92,6 +97,9 @@ class TwoFactorChallengeControllerTest extends TestCase {
$provider = $this->getMockBuilder('\OCP\Authentication\TwoFactorAuth\IProvider') $provider = $this->getMockBuilder('\OCP\Authentication\TwoFactorAuth\IProvider')
->disableOriginalConstructor() ->disableOriginalConstructor()
->getMock(); ->getMock();
$backupProvider = $this->getMockBuilder('\OCP\Authentication\TwoFactorAuth\IProvider')
->disableOriginalConstructor()
->getMock();
$tmpl = $this->getMockBuilder('\OCP\Template') $tmpl = $this->getMockBuilder('\OCP\Template')
->disableOriginalConstructor() ->disableOriginalConstructor()
->getMock(); ->getMock();
@ -103,6 +111,16 @@ class TwoFactorChallengeControllerTest extends TestCase {
->method('getProvider') ->method('getProvider')
->with($user, 'myprovider') ->with($user, 'myprovider')
->will($this->returnValue($provider)); ->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()) $this->session->expects($this->once())
->method('exists') ->method('exists')
@ -122,6 +140,7 @@ class TwoFactorChallengeControllerTest extends TestCase {
$expected = new \OCP\AppFramework\Http\TemplateResponse('core', 'twofactorshowchallenge', [ $expected = new \OCP\AppFramework\Http\TemplateResponse('core', 'twofactorshowchallenge', [
'error' => true, 'error' => true,
'provider' => $provider, 'provider' => $provider,
'backupProvider' => $backupProvider,
'logout_attribute' => 'logoutAttribute', 'logout_attribute' => 'logoutAttribute',
'template' => '<html/>', 'template' => '<html/>',
], 'guest'); ], 'guest');

View File

@ -306,7 +306,8 @@ class ManagerTest extends TestCase {
$this->appConfig->setValue('test1', 'enabled', 'yes'); $this->appConfig->setValue('test1', 'enabled', 'yes');
$this->appConfig->setValue('test2', 'enabled', 'no'); $this->appConfig->setValue('test2', 'enabled', 'no');
$this->appConfig->setValue('test3', 'enabled', '["foo"]'); $this->appConfig->setValue('test3', 'enabled', '["foo"]');
$this->assertEquals(['dav', 'federatedfilesharing', 'files', 'test1', 'test3', 'workflowengine'], $this->manager->getInstalledApps()); $apps = ['dav', 'federatedfilesharing', 'files', 'test1', 'test3', 'twofactor_backupcodes', 'workflowengine'];
$this->assertEquals($apps, $this->manager->getInstalledApps());
} }
public function testGetAppsForUser() { public function testGetAppsForUser() {
@ -320,7 +321,16 @@ class ManagerTest extends TestCase {
$this->appConfig->setValue('test2', 'enabled', 'no'); $this->appConfig->setValue('test2', 'enabled', 'no');
$this->appConfig->setValue('test3', 'enabled', '["foo"]'); $this->appConfig->setValue('test3', 'enabled', '["foo"]');
$this->appConfig->setValue('test4', 'enabled', '["asd"]'); $this->appConfig->setValue('test4', 'enabled', '["asd"]');
$this->assertEquals(['dav', 'federatedfilesharing', 'files', 'test1', 'test3', 'workflowengine'], $this->manager->getEnabledAppsForUser($user)); $enabled = [
'dav',
'federatedfilesharing',
'files',
'test1',
'test3',
'twofactor_backupcodes',
'workflowengine'
];
$this->assertEquals($enabled, $this->manager->getEnabledAppsForUser($user));
} }
public function testGetAppsNeedingUpgrade() { public function testGetAppsNeedingUpgrade() {
@ -338,6 +348,7 @@ class ManagerTest extends TestCase {
'test3' => ['id' => 'test3', 'version' => '1.2.4', 'requiremin' => '9.0.0'], 'test3' => ['id' => 'test3', 'version' => '1.2.4', 'requiremin' => '9.0.0'],
'test4' => ['id' => 'test4', 'version' => '3.0.0', 'requiremin' => '8.1.0'], 'test4' => ['id' => 'test4', 'version' => '3.0.0', 'requiremin' => '8.1.0'],
'testnoversion' => ['id' => 'testnoversion', 'requiremin' => '8.2.0'], 'testnoversion' => ['id' => 'testnoversion', 'requiremin' => '8.2.0'],
'twofactor_backupcodes' => ['id' => 'twofactor_backupcodes'],
'workflowengine' => ['id' => 'workflowengine'], 'workflowengine' => ['id' => 'workflowengine'],
]; ];
@ -379,6 +390,7 @@ class ManagerTest extends TestCase {
'test2' => ['id' => 'test2', 'version' => '1.0.0', 'requiremin' => '8.2.0'], 'test2' => ['id' => 'test2', 'version' => '1.0.0', 'requiremin' => '8.2.0'],
'test3' => ['id' => 'test3', 'version' => '1.2.4', 'requiremin' => '9.0.0'], 'test3' => ['id' => 'test3', 'version' => '1.2.4', 'requiremin' => '9.0.0'],
'testnoversion' => ['id' => 'testnoversion', 'requiremin' => '8.2.0'], 'testnoversion' => ['id' => 'testnoversion', 'requiremin' => '8.2.0'],
'twofactor_backupcodes' => ['id' => 'twofactor_backupcodes'],
'workflowengine' => ['id' => 'workflowengine'], 'workflowengine' => ['id' => 'workflowengine'],
]; ];

View File

@ -316,6 +316,7 @@ class AppTest extends \Test\TestCase {
'appforgroup12', 'appforgroup12',
'dav', 'dav',
'federatedfilesharing', 'federatedfilesharing',
'twofactor_backupcodes',
'workflowengine', 'workflowengine',
), ),
false false
@ -331,6 +332,7 @@ class AppTest extends \Test\TestCase {
'appforgroup2', 'appforgroup2',
'dav', 'dav',
'federatedfilesharing', 'federatedfilesharing',
'twofactor_backupcodes',
'workflowengine', 'workflowengine',
), ),
false false
@ -347,6 +349,7 @@ class AppTest extends \Test\TestCase {
'appforgroup2', 'appforgroup2',
'dav', 'dav',
'federatedfilesharing', 'federatedfilesharing',
'twofactor_backupcodes',
'workflowengine', 'workflowengine',
), ),
false false
@ -363,6 +366,7 @@ class AppTest extends \Test\TestCase {
'appforgroup2', 'appforgroup2',
'dav', 'dav',
'federatedfilesharing', 'federatedfilesharing',
'twofactor_backupcodes',
'workflowengine', 'workflowengine',
), ),
false, false,
@ -379,6 +383,7 @@ class AppTest extends \Test\TestCase {
'appforgroup2', 'appforgroup2',
'dav', 'dav',
'federatedfilesharing', 'federatedfilesharing',
'twofactor_backupcodes',
'workflowengine', 'workflowengine',
), ),
true, true,
@ -457,11 +462,11 @@ class AppTest extends \Test\TestCase {
); );
$apps = \OC_App::getEnabledApps(); $apps = \OC_App::getEnabledApps();
$this->assertEquals(array('files', 'app3', 'dav', 'federatedfilesharing', 'workflowengine'), $apps); $this->assertEquals(array('files', 'app3', 'dav', 'federatedfilesharing', 'twofactor_backupcodes', 'workflowengine'), $apps);
// mock should not be called again here // mock should not be called again here
$apps = \OC_App::getEnabledApps(); $apps = \OC_App::getEnabledApps();
$this->assertEquals(array('files', 'app3', 'dav', 'federatedfilesharing', 'workflowengine'), $apps); $this->assertEquals(array('files', 'app3', 'dav', 'federatedfilesharing', 'twofactor_backupcodes', 'workflowengine'), $apps);
$this->restoreAppConfig(); $this->restoreAppConfig();
\OC_User::setUserId(null); \OC_User::setUserId(null);

View File

@ -22,54 +22,76 @@
namespace Test\Authentication\TwoFactorAuth; namespace Test\Authentication\TwoFactorAuth;
use Test\TestCase; use Exception;
use OC;
use OC\App\AppManager;
use OC\Authentication\TwoFactorAuth\Manager; 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 { class ManagerTest extends TestCase {
/** @var \OCP\IUser|\PHPUnit_Framework_MockObject_MockObject */ /** @var IUser|PHPUnit_Framework_MockObject_MockObject */
private $user; private $user;
/** @var \OC\App\AppManager|\PHPUnit_Framework_MockObject_MockObject */ /** @var AppManager|PHPUnit_Framework_MockObject_MockObject */
private $appManager; private $appManager;
/** @var \OCP\ISession|\PHPUnit_Framework_MockObject_MockObject */ /** @var ISession|PHPUnit_Framework_MockObject_MockObject */
private $session; private $session;
/** @var Manager */ /** @var Manager */
private $manager; private $manager;
/** @var \OCP\IConfig|\PHPUnit_Framework_MockObject_MockObject */ /** @var IConfig|PHPUnit_Framework_MockObject_MockObject */
private $config; private $config;
/** @var \OCP\Authentication\TwoFactorAuth\IProvider|\PHPUnit_Framework_MockObject_MockObject */ /** @var IProvider|PHPUnit_Framework_MockObject_MockObject */
private $fakeProvider; private $fakeProvider;
/** @var IProvider|PHPUnit_Framework_MockObject_MockObject */
private $backupProvider;
protected function setUp() { protected function setUp() {
parent::setUp(); parent::setUp();
$this->user = $this->getMock('\OCP\IUser'); $this->user = $this->getMockBuilder('\OCP\IUser')->getMock();
$this->appManager = $this->getMockBuilder('\OC\App\AppManager') $this->appManager = $this->getMockBuilder('\OC\App\AppManager')
->disableOriginalConstructor() ->disableOriginalConstructor()
->getMock(); ->getMock();
$this->session = $this->getMock('\OCP\ISession'); $this->session = $this->getMockBuilder('\OCP\ISession')->getMock();
$this->config = $this->getMock('\OCP\IConfig'); $this->config = $this->getMockBuilder('\OCP\IConfig')->getMock();
$this->manager = $this->getMockBuilder('\OC\Authentication\TwoFactorAuth\Manager') $this->manager = $this->getMockBuilder('\OC\Authentication\TwoFactorAuth\Manager')
->setConstructorArgs([$this->appManager, $this->session, $this->config]) ->setConstructorArgs([$this->appManager, $this->session, $this->config])
->setMethods(['loadTwoFactorApp']) // Do not actually load the apps ->setMethods(['loadTwoFactorApp']) // Do not actually load the apps
->getMock(); ->getMock();
$this->fakeProvider = $this->getMock('\OCP\Authentication\TwoFactorAuth\IProvider'); $this->fakeProvider = $this->getMockBuilder('\OCP\Authentication\TwoFactorAuth\IProvider')->getMock();
$this->fakeProvider->expects($this->any()) $this->fakeProvider->expects($this->any())
->method('getId') ->method('getId')
->will($this->returnValue('email')); ->will($this->returnValue('email'));
$this->fakeProvider->expects($this->any()) $this->fakeProvider->expects($this->any())
->method('isTwoFactorAuthEnabledForUser') ->method('isTwoFactorAuthEnabledForUser')
->will($this->returnValue(true)); ->will($this->returnValue(true));
\OC::$server->registerService('\OCA\MyCustom2faApp\FakeProvider', function() { OC::$server->registerService('\OCA\MyCustom2faApp\FakeProvider', function() {
return $this->fakeProvider; 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() { private function prepareNoProviders() {
@ -105,8 +127,40 @@ class ManagerTest extends TestCase {
->with('mycustom2faapp'); ->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 * @expectedExceptionMessage Could not load two-factor auth provider \OCA\MyFaulty2faApp\DoesNotExist
*/ */
public function testFailHardIfProviderCanNotBeLoaded() { public function testFailHardIfProviderCanNotBeLoaded() {
@ -150,6 +204,12 @@ class ManagerTest extends TestCase {
$this->assertSame($this->fakeProvider, $this->manager->getProvider($this->user, 'email')); $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() { public function testGetInvalidProvider() {
$this->prepareProviders(); $this->prepareProviders();

View File

@ -25,7 +25,7 @@
// We only can count up. The 4. digit is only for the internal patchlevel to trigger DB upgrades // We only can count up. The 4. digit is only for the internal patchlevel to trigger DB upgrades
// between betas, final and RCs. This is _not_ the public version number. Reset minor/patchlevel // between betas, final and RCs. This is _not_ the public version number. Reset minor/patchlevel
// when updating major/minor version number. // when updating major/minor version number.
$OC_Version = array(9, 2, 0, 2); $OC_Version = array(9, 2, 0, 3);
// The human readable string // The human readable string
$OC_VersionString = '11.0 alpha'; $OC_VersionString = '11.0 alpha';