Merge pull request #1171 from nextcloud/2fa-backup-codes
add 2fa backup codes app
This commit is contained in:
commit
f8eb7be7b1
|
@ -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
|
||||||
|
|
|
@ -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');
|
|
@ -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>
|
|
@ -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>
|
|
@ -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'
|
||||||
|
],
|
||||||
|
]
|
||||||
|
];
|
|
@ -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;
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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, $, _);
|
|
@ -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),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$tmpl = new \OCP\Template('twofactor_backupcodes', 'personal');
|
||||||
|
|
||||||
|
return $tmpl->fetchPage();
|
|
@ -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>
|
|
@ -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>
|
|
@ -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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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'));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -293,6 +293,7 @@ Feature: provisioning
|
||||||
| provisioning_api |
|
| provisioning_api |
|
||||||
| systemtags |
|
| systemtags |
|
||||||
| theming |
|
| theming |
|
||||||
|
| twofactor_backupcodes |
|
||||||
| updatenotification |
|
| updatenotification |
|
||||||
| workflowengine |
|
| workflowengine |
|
||||||
| files_external |
|
| files_external |
|
||||||
|
|
|
@ -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(),
|
||||||
];
|
];
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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'];
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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'],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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';
|
||||||
|
|
Loading…
Reference in New Issue