* add backup codes app unit tests * add integration tests for the backup codes apppull/1171/head
parent
8b484eedf0
commit
8acb734854
@ -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')); |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue