add 2fa backup codes app
* add backup codes app unit tests * add integration tests for the backup codes app
This commit is contained in:
parent
8b484eedf0
commit
8acb734854
|
@ -27,6 +27,7 @@
|
|||
!/apps/admin_audit
|
||||
!/apps/updatenotification
|
||||
!/apps/theming
|
||||
!/apps/twofactor_backupcodes
|
||||
!/apps/workflowengine
|
||||
/apps/files_external/3rdparty/irodsphp/PHPUnitTest
|
||||
/apps/files_external/3rdparty/irodsphp/web
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
|
||||
}
|
|
@ -82,9 +82,11 @@ class TwoFactorChallengeController extends Controller {
|
|||
public function selectChallenge($redirect_url) {
|
||||
$user = $this->userSession->getUser();
|
||||
$providers = $this->twoFactorManager->getProviders($user);
|
||||
$backupProvider = $this->twoFactorManager->getBackupProvider($user);
|
||||
|
||||
$data = [
|
||||
'providers' => $providers,
|
||||
'backupProvider' => $backupProvider,
|
||||
'redirect_url' => $redirect_url,
|
||||
'logout_attribute' => $this->getLogoutAttribute(),
|
||||
];
|
||||
|
@ -107,6 +109,12 @@ class TwoFactorChallengeController extends Controller {
|
|||
return new RedirectResponse($this->urlGenerator->linkToRoute('core.TwoFactorChallenge.selectChallenge'));
|
||||
}
|
||||
|
||||
$backupProvider = $this->twoFactorManager->getBackupProvider($user);
|
||||
if (!is_null($backupProvider) && $backupProvider->getId() === $provider->getId()) {
|
||||
// Don't show the backup provider link if we're already showing that provider's challenge
|
||||
$backupProvider = null;
|
||||
}
|
||||
|
||||
if ($this->session->exists('two_factor_auth_error')) {
|
||||
$this->session->remove('two_factor_auth_error');
|
||||
$error = true;
|
||||
|
@ -118,6 +126,7 @@ class TwoFactorChallengeController extends Controller {
|
|||
$data = [
|
||||
'error' => $error,
|
||||
'provider' => $provider,
|
||||
'backupProvider' => $backupProvider,
|
||||
'logout_attribute' => $this->getLogoutAttribute(),
|
||||
'template' => $tmpl->fetchPage(),
|
||||
];
|
||||
|
|
|
@ -45,7 +45,7 @@ body {
|
|||
border: none !important;
|
||||
}
|
||||
|
||||
.two-factor-cancel {
|
||||
.two-factor-link {
|
||||
display: inline-block;
|
||||
padding: 12px;
|
||||
color: rgba(255, 255, 255, .75);
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
"survey_client",
|
||||
"systemtags",
|
||||
"templateeditor",
|
||||
"twofactor_backupcodes",
|
||||
"theming",
|
||||
"updatenotification",
|
||||
"user_external",
|
||||
|
@ -39,6 +40,7 @@
|
|||
"files",
|
||||
"dav",
|
||||
"federatedfilesharing",
|
||||
"twofactor_backupcodes",
|
||||
"workflowengine"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -19,4 +19,12 @@
|
|||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
<a class="two-factor-cancel" <?php print_unescaped($_['logout_attribute']); ?>><?php p($l->t('Cancel log in')) ?></a>
|
||||
<a class="two-factor-link" <?php print_unescaped($_['logout_attribute']); ?>><?php p($l->t('Cancel log in')) ?></a>
|
||||
<?php if (!is_null($_['backupProvider'])): ?>
|
||||
<a class="two-factor-link" href="<?php p(\OC::$server->getURLGenerator()->linkToRoute('core.TwoFactorChallenge.showChallenge',
|
||||
[
|
||||
'challengeProviderId' => $_['backupProvider']->getId(),
|
||||
'redirect_url' => $_['redirect_url'],
|
||||
]
|
||||
)) ?>"><?php p($l->t('Use backup code')) ?></a>
|
||||
<?php endif;
|
||||
|
|
|
@ -16,4 +16,12 @@ $template = $_['template'];
|
|||
<?php endif; ?>
|
||||
<?php print_unescaped($template); ?>
|
||||
</div>
|
||||
<a class="two-factor-cancel" <?php print_unescaped($_['logout_attribute']); ?>><?php p($l->t('Cancel log in')) ?></a>
|
||||
<a class="two-factor-link" <?php print_unescaped($_['logout_attribute']); ?>><?php p($l->t('Cancel log in')) ?></a>
|
||||
<?php if (!is_null($_['backupProvider'])): ?>
|
||||
<a class="two-factor-link" href="<?php p(\OC::$server->getURLGenerator()->linkToRoute('core.TwoFactorChallenge.showChallenge',
|
||||
[
|
||||
'challengeProviderId' => $_['backupProvider']->getId(),
|
||||
'redirect_url' => $_['redirect_url'],
|
||||
]
|
||||
)) ?>"><?php p($l->t('Use backup code')) ?></a>
|
||||
<?php endif;
|
||||
|
|
|
@ -35,6 +35,8 @@ use OCP\IUser;
|
|||
class Manager {
|
||||
|
||||
const SESSION_UID_KEY = 'two_factor_auth_uid';
|
||||
const BACKUP_CODES_APP_ID = 'twofactor_backupcodes';
|
||||
const BACKUP_CODES_PROVIDER_ID = 'backup_codes';
|
||||
|
||||
/** @var AppManager */
|
||||
private $appManager;
|
||||
|
@ -93,21 +95,35 @@ class Manager {
|
|||
* @return IProvider|null
|
||||
*/
|
||||
public function getProvider(IUser $user, $challengeProviderId) {
|
||||
$providers = $this->getProviders($user);
|
||||
$providers = $this->getProviders($user, true);
|
||||
return isset($providers[$challengeProviderId]) ? $providers[$challengeProviderId] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param IUser $user
|
||||
* @return IProvider|null the backup provider, if enabled for the given user
|
||||
*/
|
||||
public function getBackupProvider(IUser $user) {
|
||||
$providers = $this->getProviders($user, true);
|
||||
return $providers[self::BACKUP_CODES_PROVIDER_ID];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of 2FA providers for the given user
|
||||
*
|
||||
* @param IUser $user
|
||||
* @param bool $includeBackupApp
|
||||
* @return IProvider[]
|
||||
*/
|
||||
public function getProviders(IUser $user) {
|
||||
public function getProviders(IUser $user, $includeBackupApp = false) {
|
||||
$allApps = $this->appManager->getEnabledAppsForUser($user);
|
||||
$providers = [];
|
||||
|
||||
foreach ($allApps as $appId) {
|
||||
if (!$includeBackupApp && $appId === self::BACKUP_CODES_APP_ID) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$info = $this->appManager->getAppInfo($appId);
|
||||
if (isset($info['two-factor-providers'])) {
|
||||
$providerClasses = $info['two-factor-providers'];
|
||||
|
|
|
@ -77,9 +77,14 @@ class TwoFactorChallengeControllerTest extends TestCase {
|
|||
->method('getProviders')
|
||||
->with($user)
|
||||
->will($this->returnValue($providers));
|
||||
$this->twoFactorManager->expects($this->once())
|
||||
->method('getBackupProvider')
|
||||
->with($user)
|
||||
->will($this->returnValue('backup'));
|
||||
|
||||
$expected = new \OCP\AppFramework\Http\TemplateResponse('core', 'twofactorselectchallenge', [
|
||||
'providers' => $providers,
|
||||
'backupProvider' => 'backup',
|
||||
'redirect_url' => '/some/url',
|
||||
'logout_attribute' => 'logoutAttribute',
|
||||
], 'guest');
|
||||
|
@ -92,6 +97,9 @@ class TwoFactorChallengeControllerTest extends TestCase {
|
|||
$provider = $this->getMockBuilder('\OCP\Authentication\TwoFactorAuth\IProvider')
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$backupProvider = $this->getMockBuilder('\OCP\Authentication\TwoFactorAuth\IProvider')
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$tmpl = $this->getMockBuilder('\OCP\Template')
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
|
@ -103,6 +111,16 @@ class TwoFactorChallengeControllerTest extends TestCase {
|
|||
->method('getProvider')
|
||||
->with($user, 'myprovider')
|
||||
->will($this->returnValue($provider));
|
||||
$this->twoFactorManager->expects($this->once())
|
||||
->method('getBackupProvider')
|
||||
->with($user)
|
||||
->will($this->returnValue($backupProvider));
|
||||
$provider->expects($this->once())
|
||||
->method('getId')
|
||||
->will($this->returnValue('u2f'));
|
||||
$backupProvider->expects($this->once())
|
||||
->method('getId')
|
||||
->will($this->returnValue('backup_codes'));
|
||||
|
||||
$this->session->expects($this->once())
|
||||
->method('exists')
|
||||
|
@ -122,6 +140,7 @@ class TwoFactorChallengeControllerTest extends TestCase {
|
|||
$expected = new \OCP\AppFramework\Http\TemplateResponse('core', 'twofactorshowchallenge', [
|
||||
'error' => true,
|
||||
'provider' => $provider,
|
||||
'backupProvider' => $backupProvider,
|
||||
'logout_attribute' => 'logoutAttribute',
|
||||
'template' => '<html/>',
|
||||
], 'guest');
|
||||
|
|
|
@ -22,54 +22,76 @@
|
|||
|
||||
namespace Test\Authentication\TwoFactorAuth;
|
||||
|
||||
use Test\TestCase;
|
||||
use Exception;
|
||||
use OC;
|
||||
use OC\App\AppManager;
|
||||
use OC\Authentication\TwoFactorAuth\Manager;
|
||||
use OCA\TwoFactor_BackupCodes\Provider\BackupCodesProvider;
|
||||
use OCP\Authentication\TwoFactorAuth\IProvider;
|
||||
use OCP\IConfig;
|
||||
use OCP\ISession;
|
||||
use OCP\IUser;
|
||||
use Test\TestCase;
|
||||
|
||||
class ManagerTest extends TestCase {
|
||||
|
||||
/** @var \OCP\IUser|\PHPUnit_Framework_MockObject_MockObject */
|
||||
/** @var IUser|PHPUnit_Framework_MockObject_MockObject */
|
||||
private $user;
|
||||
|
||||
/** @var \OC\App\AppManager|\PHPUnit_Framework_MockObject_MockObject */
|
||||
/** @var AppManager|PHPUnit_Framework_MockObject_MockObject */
|
||||
private $appManager;
|
||||
|
||||
/** @var \OCP\ISession|\PHPUnit_Framework_MockObject_MockObject */
|
||||
/** @var ISession|PHPUnit_Framework_MockObject_MockObject */
|
||||
private $session;
|
||||
|
||||
/** @var Manager */
|
||||
private $manager;
|
||||
|
||||
/** @var \OCP\IConfig|\PHPUnit_Framework_MockObject_MockObject */
|
||||
/** @var IConfig|PHPUnit_Framework_MockObject_MockObject */
|
||||
private $config;
|
||||
|
||||
/** @var \OCP\Authentication\TwoFactorAuth\IProvider|\PHPUnit_Framework_MockObject_MockObject */
|
||||
/** @var IProvider|PHPUnit_Framework_MockObject_MockObject */
|
||||
private $fakeProvider;
|
||||
|
||||
/** @var IProvider|PHPUnit_Framework_MockObject_MockObject */
|
||||
private $backupProvider;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->user = $this->getMock('\OCP\IUser');
|
||||
$this->user = $this->getMockBuilder('\OCP\IUser')->getMock();
|
||||
$this->appManager = $this->getMockBuilder('\OC\App\AppManager')
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$this->session = $this->getMock('\OCP\ISession');
|
||||
$this->config = $this->getMock('\OCP\IConfig');
|
||||
$this->session = $this->getMockBuilder('\OCP\ISession')->getMock();
|
||||
$this->config = $this->getMockBuilder('\OCP\IConfig')->getMock();
|
||||
|
||||
$this->manager = $this->getMockBuilder('\OC\Authentication\TwoFactorAuth\Manager')
|
||||
->setConstructorArgs([$this->appManager, $this->session, $this->config])
|
||||
->setMethods(['loadTwoFactorApp']) // Do not actually load the apps
|
||||
->getMock();
|
||||
|
||||
$this->fakeProvider = $this->getMock('\OCP\Authentication\TwoFactorAuth\IProvider');
|
||||
$this->fakeProvider = $this->getMockBuilder('\OCP\Authentication\TwoFactorAuth\IProvider')->getMock();
|
||||
$this->fakeProvider->expects($this->any())
|
||||
->method('getId')
|
||||
->will($this->returnValue('email'));
|
||||
$this->fakeProvider->expects($this->any())
|
||||
->method('isTwoFactorAuthEnabledForUser')
|
||||
->will($this->returnValue(true));
|
||||
\OC::$server->registerService('\OCA\MyCustom2faApp\FakeProvider', function() {
|
||||
OC::$server->registerService('\OCA\MyCustom2faApp\FakeProvider', function() {
|
||||
return $this->fakeProvider;
|
||||
});
|
||||
|
||||
$this->backupProvider = $this->getMockBuilder('\OCP\Authentication\TwoFactorAuth\IProvider')->getMock();
|
||||
$this->backupProvider->expects($this->any())
|
||||
->method('getId')
|
||||
->will($this->returnValue('backup_codes'));
|
||||
$this->backupProvider->expects($this->any())
|
||||
->method('isTwoFactorAuthEnabledForUser')
|
||||
->will($this->returnValue(true));
|
||||
OC::$server->registerService('\OCA\TwoFactor_BackupCodes\Provider\FakeBackupCodesProvider', function () {
|
||||
return $this->backupProvider;
|
||||
});
|
||||
}
|
||||
|
||||
private function prepareNoProviders() {
|
||||
|
@ -105,8 +127,40 @@ class ManagerTest extends TestCase {
|
|||
->with('mycustom2faapp');
|
||||
}
|
||||
|
||||
private function prepareProvidersWitBackupProvider() {
|
||||
$this->appManager->expects($this->any())
|
||||
->method('getEnabledAppsForUser')
|
||||
->with($this->user)
|
||||
->will($this->returnValue([
|
||||
'mycustom2faapp',
|
||||
'twofactor_backupcodes',
|
||||
]));
|
||||
|
||||
$this->appManager->expects($this->exactly(2))
|
||||
->method('getAppInfo')
|
||||
->will($this->returnValueMap([
|
||||
[
|
||||
'mycustom2faapp',
|
||||
['two-factor-providers' => [
|
||||
'\OCA\MyCustom2faApp\FakeProvider',
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
'twofactor_backupcodes',
|
||||
['two-factor-providers' => [
|
||||
'\OCA\TwoFactor_BackupCodes\Provider\FakeBackupCodesProvider',
|
||||
]
|
||||
]
|
||||
],
|
||||
]));
|
||||
|
||||
$this->manager->expects($this->exactly(2))
|
||||
->method('loadTwoFactorApp');
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Exception
|
||||
* @expectedException Exception
|
||||
* @expectedExceptionMessage Could not load two-factor auth provider \OCA\MyFaulty2faApp\DoesNotExist
|
||||
*/
|
||||
public function testFailHardIfProviderCanNotBeLoaded() {
|
||||
|
@ -150,6 +204,12 @@ class ManagerTest extends TestCase {
|
|||
$this->assertSame($this->fakeProvider, $this->manager->getProvider($this->user, 'email'));
|
||||
}
|
||||
|
||||
public function testGetBackupProvider() {
|
||||
$this->prepareProvidersWitBackupProvider();
|
||||
|
||||
$this->assertSame($this->backupProvider, $this->manager->getBackupProvider($this->user));
|
||||
}
|
||||
|
||||
public function testGetInvalidProvider() {
|
||||
$this->prepareProviders();
|
||||
|
||||
|
|
Loading…
Reference in New Issue