Refactor the 2FA plugin/drivers/storage to allow multiple factors of the same type
This commit is contained in:
parent
358ac3e33f
commit
7f3a76fdad
9 changed files with 275 additions and 165 deletions
|
@ -45,29 +45,28 @@ window.rcmail && rcmail.addEventListener('init', function(evt) {
|
|||
table.html('');
|
||||
|
||||
var rows = 0;
|
||||
$.each(rcmail.env.kolab_2fa_factors, function(method, props) {
|
||||
$.each(rcmail.env.kolab_2fa_factors, function(id, props) {
|
||||
if (props.active) {
|
||||
var tr = $('<tr>').addClass(method).appendTo(table);
|
||||
$('<td>').addClass('name').text(props.name || method).appendTo(tr);
|
||||
var tr = $('<tr>').addClass(props.method).appendTo(table);
|
||||
$('<td>').addClass('name').text(props.label || props.name).appendTo(tr);
|
||||
$('<td>').addClass('created').text(props.created || '??').appendTo(tr);
|
||||
$('<td>').addClass('actions').html('<a class="button delete" rel="'+method+'">' + rcmail.get_label('remove','kolab_2fa') + '</a>').appendTo(tr);
|
||||
$('<td>').addClass('actions').html('<a class="button delete" rel="'+id+'">' + rcmail.get_label('remove','kolab_2fa') + '</a>').appendTo(tr);
|
||||
rows++;
|
||||
}
|
||||
});
|
||||
|
||||
table.parent()[(rows > 0 ? 'show' : 'hide')]();
|
||||
|
||||
/*
|
||||
var remaining = 0;
|
||||
$('#kolab2fa-add option').each(function(i, elem) {
|
||||
var method = elem.value;
|
||||
if (rcmail.env.kolab_2fa_factors[method]) {
|
||||
$(elem).prop('disabled', rcmail.env.kolab_2fa_factors[method].active);
|
||||
if (!rcmail.env.kolab_2fa_factors[method].active) {
|
||||
remaining++;
|
||||
}
|
||||
$(elem).prop('disabled', active[method]);
|
||||
if (!active[method]) {
|
||||
remaining++;
|
||||
}
|
||||
});
|
||||
$('#kolab2fa-add').prop('disabled', !remaining).get(0).selectedIndex = 0;
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -102,7 +101,11 @@ window.rcmail && rcmail.addEventListener('init', function(evt) {
|
|||
],
|
||||
{
|
||||
open: function(event, ui) {
|
||||
|
||||
$(event.target).find('input[name="_verify_code"]').keypress(function(e) {
|
||||
if (e.which == 13) {
|
||||
$(e.target).closest('.ui-dialog').find('.ui-button.mainaction').click();
|
||||
}
|
||||
});
|
||||
},
|
||||
close: function(event, ui) {
|
||||
form.hide().appendTo(document.body);
|
||||
|
@ -128,21 +131,21 @@ window.rcmail && rcmail.addEventListener('init', function(evt) {
|
|||
/**
|
||||
* Remove the given factor from the account
|
||||
*/
|
||||
function remove_factor(method) {
|
||||
if (rcmail.env.kolab_2fa_factors[method]) {
|
||||
rcmail.env.kolab_2fa_factors[method].active = false;
|
||||
function remove_factor(id) {
|
||||
if (rcmail.env.kolab_2fa_factors[id]) {
|
||||
rcmail.env.kolab_2fa_factors[id].active = false;
|
||||
}
|
||||
render();
|
||||
|
||||
var lock = rcmail.set_busy(true, 'saving');
|
||||
rcmail.http_post('plugin.kolab-2fa-save', { _method: method, _data: 'false' }, lock);
|
||||
rcmail.http_post('plugin.kolab-2fa-save', { _method: id, _data: 'false' }, lock);
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit factor settings form
|
||||
*/
|
||||
function save_data(method) {
|
||||
var lock, form = $('#kolab2fa-prop-' + method),
|
||||
var lock, data, form = $('#kolab2fa-prop-' + method),
|
||||
verify = form.find('input[name="_verify_code"]');
|
||||
|
||||
if (verify.length && !verify.val().length) {
|
||||
|
@ -151,10 +154,11 @@ window.rcmail && rcmail.addEventListener('init', function(evt) {
|
|||
return false;
|
||||
}
|
||||
|
||||
data = form_data(form);
|
||||
lock = rcmail.set_busy(true, 'saving');
|
||||
rcmail.http_post('plugin.kolab-2fa-save', {
|
||||
_method: method,
|
||||
_data: JSON.stringify(form_data(form)),
|
||||
_method: data.id || method,
|
||||
_data: JSON.stringify(data),
|
||||
_verify_code: verify.val(),
|
||||
_timestamp: factor_dialog ? factor_dialog.data('timestamp') : null
|
||||
}, lock);
|
||||
|
@ -181,18 +185,20 @@ window.rcmail && rcmail.addEventListener('init', function(evt) {
|
|||
/**
|
||||
* Execute the given function after the user authorized the session with a 2nd factor
|
||||
*/
|
||||
function require_high_security(func)
|
||||
function require_high_security(func, exclude)
|
||||
{
|
||||
// request 2nd factor auth
|
||||
if (!rcmail.env.session_secured || rcmail.env.session_secured < time() - 120) {
|
||||
var method, name;
|
||||
|
||||
// find an active factor
|
||||
$.each(rcmail.env.kolab_2fa_factors, function(m, prop) {
|
||||
if (prop.active) {
|
||||
method = m;
|
||||
name = prop.name;
|
||||
return true;
|
||||
$.each(rcmail.env.kolab_2fa_factors, function(id, prop) {
|
||||
if (prop.active && !method || method == exclude) {
|
||||
method = id;
|
||||
name = prop.label || prop.name;
|
||||
if (!exclude || id !== exclude) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -259,12 +265,12 @@ window.rcmail && rcmail.addEventListener('init', function(evt) {
|
|||
|
||||
// callback for factor data provided by the server
|
||||
rcmail.addEventListener('plugin.render_data', function(data) {
|
||||
var method = data._method,
|
||||
var method = data.method,
|
||||
form = $('#kolab2fa-prop-' + method);
|
||||
|
||||
if (form.length) {
|
||||
$.each(data, function(field, value) {
|
||||
form.find('[name="_prop[' + method + '][' + field + ']"]').val(value);
|
||||
form.find('[name="_prop[' + field + ']"]').val(value);
|
||||
});
|
||||
|
||||
if (data.qrcode) {
|
||||
|
@ -278,8 +284,14 @@ window.rcmail && rcmail.addEventListener('init', function(evt) {
|
|||
|
||||
// callback for save action
|
||||
rcmail.addEventListener('plugin.save_success', function(data) {
|
||||
if (rcmail.env.kolab_2fa_factors[data.method]) {
|
||||
$.extend(rcmail.env.kolab_2fa_factors[data.method], data);
|
||||
if (!data.active && rcmail.env.kolab_2fa_factors[data.id]) {
|
||||
delete rcmail.env.kolab_2fa_factors[data.id];
|
||||
}
|
||||
else if (rcmail.env.kolab_2fa_factors[data.id]) {
|
||||
$.extend(rcmail.env.kolab_2fa_factors[data.id], data);
|
||||
}
|
||||
else {
|
||||
rcmail.env.kolab_2fa_factors[data.id] = data;
|
||||
}
|
||||
|
||||
if (factor_dialog) {
|
||||
|
@ -334,14 +346,14 @@ window.rcmail && rcmail.addEventListener('init', function(evt) {
|
|||
|
||||
// handler for delete button clicks
|
||||
$('#kolab2fa-factors tbody').on('click', '.button.delete', function(e) {
|
||||
var method = $(this).attr('rel');
|
||||
var id = $(this).attr('rel');
|
||||
|
||||
// require auth verification
|
||||
require_high_security(function() {
|
||||
if (confirm(rcmail.get_label('authremoveconfirm', 'kolab_2fa'))) {
|
||||
remove_factor(method);
|
||||
remove_factor(id);
|
||||
}
|
||||
});
|
||||
}, id);
|
||||
|
||||
return false;
|
||||
});
|
||||
|
|
|
@ -107,7 +107,7 @@ class kolab_2fa extends rcube_plugin
|
|||
}
|
||||
// 2b. check storage if this user has 2FA enabled
|
||||
else if ($storage = $this->get_storage($args['user'])) {
|
||||
$factors = (array)$storage->read('active');
|
||||
$factors = (array)$storage->enumerate();
|
||||
}
|
||||
|
||||
if (count($factors) > 0) {
|
||||
|
@ -155,19 +155,20 @@ class kolab_2fa extends rcube_plugin
|
|||
$time = $_SESSION['kolab_2fa_time'];
|
||||
$nonce = $_SESSION['kolab_2fa_nonce'];
|
||||
$factors = (array)$_SESSION['kolab_2fa_factors'];
|
||||
$sign = rcube_utils::get_input_value('_sign', rcube_utils::INPUT_POST);
|
||||
|
||||
$this->login_verified = false;
|
||||
$expired = $time < time() - $rcmail->config->get('kolab_2fa_timeout', 120);
|
||||
|
||||
if (!empty($sign) && !empty($factors) && !empty($nonce) && !$expired) {
|
||||
if (!empty($factors) && !empty($nonce) && !$expired) {
|
||||
// TODO: check signature
|
||||
|
||||
// try to verify each configured factor
|
||||
foreach ($factors as $method) {
|
||||
foreach ($factors as $factor) {
|
||||
list($method) = explode(':', $factor, 2);
|
||||
|
||||
// verify the submitted code
|
||||
$code = rcube_utils::get_input_value("_${nonce}_${method}", rcube_utils::INPUT_POST);
|
||||
$this->login_verified = $this->verify_factor_auth($method, $code);
|
||||
$this->login_verified = $this->verify_factor_auth($factor, $code);
|
||||
|
||||
// accept first successful method
|
||||
if ($this->login_verified) {
|
||||
|
@ -225,21 +226,26 @@ class kolab_2fa extends rcube_plugin
|
|||
$form_name = !empty($attrib['form']) ? $attrib['form'] : 'form';
|
||||
$nonce = $_SESSION['kolab_2fa_nonce'];
|
||||
|
||||
$methods = array_unique(array_map(function($factor) {
|
||||
list($method, $id) = explode(':', $factor);
|
||||
return $method;
|
||||
},
|
||||
$this->login_factors
|
||||
));
|
||||
|
||||
// forward these values as the regular login screen would submit them
|
||||
$input_task = new html_hiddenfield(array('name' => '_task', 'value' => 'login'));
|
||||
$input_action = new html_hiddenfield(array('name' => '_action', 'value' => 'plugin.kolab-2fa-login'));
|
||||
$input_tzone = new html_hiddenfield(array('name' => '_timezone', 'id' => 'rcmlogintz', 'value' => rcube_utils::get_input_value('_timezone', rcube_utils::INPUT_POST)));
|
||||
$input_url = new html_hiddenfield(array('name' => '_url', 'id' => 'rcmloginurl', 'value' => rcube_utils::get_input_value('_url', rcube_utils::INPUT_POST)));
|
||||
// TODO: generate request signature
|
||||
$input_sign = new html_hiddenfield(array('name' => '_sign', 'id' => 'rcmloginsign', 'value' => 'XXX'));
|
||||
|
||||
// create HTML table with two cols
|
||||
$table = new html_table(array('cols' => 2));
|
||||
$required = count($this->login_factors) > 1 ? null : 'required';
|
||||
$required = count($methods) > 1 ? null : 'required';
|
||||
|
||||
// render input for each configured auth method
|
||||
foreach ($this->login_factors as $i => $method) {
|
||||
if ($i > 0) {
|
||||
foreach ($methods as $i => $method) {
|
||||
if ($row++ > 0) {
|
||||
$table->add(array('colspan' => 2, 'class' => 'title hint', 'style' => 'text-align:center'),
|
||||
$this->gettext('or'));
|
||||
}
|
||||
|
@ -255,7 +261,6 @@ class kolab_2fa extends rcube_plugin
|
|||
$out .= $input_action->show();
|
||||
$out .= $input_tzone->show();
|
||||
$out .= $input_url->show();
|
||||
$out .= $input_sign->show();
|
||||
$out .= $table->show();
|
||||
|
||||
// add submit button
|
||||
|
@ -279,12 +284,6 @@ class kolab_2fa extends rcube_plugin
|
|||
public function get_driver($method)
|
||||
{
|
||||
$rcmail = rcmail::get_instance();
|
||||
$method = strtolower($method);
|
||||
$valid = in_array($method, $rcmail->config->get('kolab_2fa_drivers', array()));
|
||||
|
||||
if (!$valid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->drivers[$method]) {
|
||||
return $this->drivers[$method];
|
||||
|
@ -404,13 +403,11 @@ class kolab_2fa extends rcube_plugin
|
|||
public function settings_factoradder($attrib)
|
||||
{
|
||||
$rcmail = rcmail::get_instance();
|
||||
$storage = $this->get_storage($rcmail->get_user_name());
|
||||
$active = $storage ? (array)$storage->read('active') : array();
|
||||
|
||||
$select = new html_select(array('id' => 'kolab2fa-add'));
|
||||
$select->add($this->gettext('addfactor') . '...', '');
|
||||
foreach ((array)$rcmail->config->get('kolab_2fa_drivers', array()) as $method) {
|
||||
$select->add($this->gettext($method), $method, array('disabled' => in_array($method, $active)));
|
||||
$select->add($this->gettext($method), $method);
|
||||
}
|
||||
|
||||
return $select->show();
|
||||
|
@ -438,30 +435,37 @@ class kolab_2fa extends rcube_plugin
|
|||
{
|
||||
$rcmail = rcmail::get_instance();
|
||||
$storage = $this->get_storage($rcmail->get_user_name());
|
||||
$factors = $storage ? (array)$storage->read('active') : array();
|
||||
$factors = $storage ? (array)$storage->enumerate() : array();
|
||||
$drivers = (array)$rcmail->config->get('kolab_2fa_drivers', array());
|
||||
$env_methods = array();
|
||||
|
||||
foreach ($drivers as $j => $method) {
|
||||
$out .= $this->settings_factor($method, $attrib);
|
||||
$env_methods[$method] = array(
|
||||
'name' => $this->gettext($method),
|
||||
'active' => 0,
|
||||
);
|
||||
}
|
||||
|
||||
$me = $this;
|
||||
$this->api->output->set_env('kolab_2fa_factors', array_combine(
|
||||
$drivers,
|
||||
array_map(function($method) use ($me, $factors) {
|
||||
$props = array(
|
||||
'name' => $me->gettext($method),
|
||||
'active' => in_array($method, $factors),
|
||||
);
|
||||
$factors,
|
||||
array_map(function($id) use ($me, &$env_methods) {
|
||||
$props = array('id' => $id);
|
||||
|
||||
if ($props['active'] && ($driver = $me->get_driver($method))) {
|
||||
if ($driver = $me->get_driver($id)) {
|
||||
$props += $this->format_props($driver->props());
|
||||
$props['method'] = $driver->method;
|
||||
$props['name'] = $me->gettext($driver->method);
|
||||
$env_methods[$driver->method]['active']++;
|
||||
}
|
||||
|
||||
return $props;
|
||||
}, $drivers)
|
||||
}, $factors)
|
||||
));
|
||||
|
||||
$this->api->output->set_env('kolab_2fa_methods', $env_methods);
|
||||
|
||||
return html::div(array('id' => 'kolab2fapropform'), $out);
|
||||
}
|
||||
|
||||
|
@ -528,6 +532,8 @@ class kolab_2fa extends rcube_plugin
|
|||
|
||||
}
|
||||
|
||||
$input_id = new html_hiddenfield(array('name' => '_prop[id]', 'value' => ''));
|
||||
|
||||
$out .= html::tag('form', array(
|
||||
'method' => 'post',
|
||||
'action' => '#',
|
||||
|
@ -536,7 +542,8 @@ class kolab_2fa extends rcube_plugin
|
|||
),
|
||||
html::tag('fieldset', array(),
|
||||
html::tag('legend', array(), $this->gettext($method)) .
|
||||
html::div('factorprop', $table->show())
|
||||
html::div('factorprop', $table->show()) .
|
||||
$input_id->show()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -566,11 +573,10 @@ class kolab_2fa extends rcube_plugin
|
|||
public function settings_save()
|
||||
{
|
||||
$method = rcube_utils::get_input_value('_method', rcube_utils::INPUT_POST);
|
||||
$data = @json_decode(rcube_utils::get_input_value('_data', rcube_utils::INPUT_POST), true);
|
||||
$data = @json_decode(rcube_utils::get_input_value('_data', rcube_utils::INPUT_POST), true);
|
||||
|
||||
$rcmail = rcmail::get_instance();
|
||||
$storage = $this->get_storage($rcmail->get_user_name());
|
||||
$active = $storage ? (array)$storage->read('active') : array();
|
||||
$success = false;
|
||||
$errors = 0;
|
||||
$save_data = array();
|
||||
|
@ -579,8 +585,7 @@ class kolab_2fa extends rcube_plugin
|
|||
if ($data === false) {
|
||||
if ($this->check_secure_mode()) {
|
||||
// remove method from active factors and clear stored settings
|
||||
$active = array_filter($active, function($f) use ($method) { return $f != $method; });
|
||||
$driver->clear();
|
||||
$success = $driver->clear();
|
||||
}
|
||||
else {
|
||||
$errors++;
|
||||
|
@ -593,9 +598,10 @@ class kolab_2fa extends rcube_plugin
|
|||
if (!empty($verify_code)) {
|
||||
if (!$driver->verify($verify_code, $timestamp)) {
|
||||
$this->api->output->command('plugin.verify_response', array(
|
||||
'method' => $method,
|
||||
'id' => $driver->id,
|
||||
'method' => $driver->method,
|
||||
'success' => false,
|
||||
'message' => str_replace('$method', $this->gettext($method), $this->gettext('codeverificationfailed'))
|
||||
'message' => str_replace('$method', $this->gettext($driver->method), $this->gettext('codeverificationfailed'))
|
||||
));
|
||||
$this->api->output->send();
|
||||
}
|
||||
|
@ -607,14 +613,12 @@ class kolab_2fa extends rcube_plugin
|
|||
}
|
||||
}
|
||||
|
||||
if (!in_array($method, $active)) {
|
||||
$active[] = $method;
|
||||
}
|
||||
$driver->set('active', true);
|
||||
}
|
||||
|
||||
// update list of active factors for this user
|
||||
if (!$errors) {
|
||||
$success = $storage && $storage->write('active', $active);
|
||||
$success = $driver->commit();
|
||||
$save_data = $data !== false ? $this->format_props($driver->props()) : array();
|
||||
}
|
||||
}
|
||||
|
@ -639,14 +643,7 @@ class kolab_2fa extends rcube_plugin
|
|||
$method = rcube_utils::get_input_value('_method', rcube_utils::INPUT_POST);
|
||||
|
||||
if ($driver = $this->get_driver($method)) {
|
||||
$data = array('_method' => $method);
|
||||
|
||||
// abort if session is not authorized
|
||||
/*
|
||||
if ($driver->active && !$this->check_secure_mode()) {
|
||||
$this->api->output->send();
|
||||
}
|
||||
*/
|
||||
$data = array('method' => $method, 'id' => $driver->id);
|
||||
|
||||
foreach ($driver->props(true) as $field => $prop) {
|
||||
$data[$field] = $prop['text'] ?: $prop['value'];
|
||||
|
@ -696,6 +693,7 @@ class kolab_2fa extends rcube_plugin
|
|||
}
|
||||
}
|
||||
$success = $driver->verify(rcube_utils::get_input_value('_code', rcube_utils::INPUT_POST), $timestamp);
|
||||
$method = $driver->method;
|
||||
}
|
||||
|
||||
// put session into high-security mode
|
||||
|
@ -726,10 +724,6 @@ class kolab_2fa extends rcube_plugin
|
|||
$value = $rcmail->format_date($prop['value']);
|
||||
break;
|
||||
|
||||
case 'boolean':
|
||||
$value = $this->gettext($prop['value'] ? 'yes' : 'no');
|
||||
break;
|
||||
|
||||
default:
|
||||
$value = $prop['value'];
|
||||
}
|
||||
|
|
|
@ -26,21 +26,45 @@ namespace Kolab2FA\Driver;
|
|||
abstract class Base
|
||||
{
|
||||
public $method = null;
|
||||
public $id = null;
|
||||
public $storage;
|
||||
|
||||
protected $config = array();
|
||||
protected $props = array();
|
||||
protected $user_props = array();
|
||||
protected $pending_changes = false;
|
||||
|
||||
protected $allowed_props = array('username');
|
||||
|
||||
public $user_settings = array();
|
||||
public $user_settings = array(
|
||||
'active' => array(
|
||||
'type' => 'boolean',
|
||||
'editable' => false,
|
||||
'hidden' => false,
|
||||
'default' => false,
|
||||
),
|
||||
'label' => array(
|
||||
'type' => 'text',
|
||||
'editable' => true,
|
||||
'label' => 'label',
|
||||
'generator' => 'default_label',
|
||||
),
|
||||
'created' => array(
|
||||
'type' => 'datetime',
|
||||
'editable' => false,
|
||||
'hidden' => false,
|
||||
'label' => 'created',
|
||||
'generator' => 'time',
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* Static factory method
|
||||
*/
|
||||
public static function factory($method, $config)
|
||||
public static function factory($id, $config)
|
||||
{
|
||||
list($method) = explode(':', $id);
|
||||
|
||||
$classmap = array(
|
||||
'totp' => '\\Kolab2FA\\Driver\\TOTP',
|
||||
'hotp' => '\\Kolab2FA\\Driver\\HOTP',
|
||||
|
@ -49,7 +73,7 @@ abstract class Base
|
|||
|
||||
$cls = $classmap[strtolower($method)];
|
||||
if ($cls && class_exists($cls)) {
|
||||
return new $cls($config);
|
||||
return new $cls($config, $id);
|
||||
}
|
||||
|
||||
throw new Exception("Unknown 2FA driver '$method'");
|
||||
|
@ -58,19 +82,26 @@ abstract class Base
|
|||
/**
|
||||
* Default constructor
|
||||
*/
|
||||
public function __construct($config = null)
|
||||
public function __construct($config = null, $id = null)
|
||||
{
|
||||
if (is_array($config)) {
|
||||
$this->init($config);
|
||||
$this->init($config);
|
||||
|
||||
if (!empty($id) && $id != $this->method) {
|
||||
$this->id = $id;
|
||||
}
|
||||
else { // generate random ID
|
||||
$this->id = $this->method . ':' . bin2hex(openssl_random_pseudo_bytes(12));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the driver with the given config options
|
||||
*/
|
||||
public function init(array $config)
|
||||
public function init($config)
|
||||
{
|
||||
$this->config = array_merge($this->config, $config);
|
||||
if (is_array($config)) {
|
||||
$this->config = array_merge($this->config, $config);
|
||||
}
|
||||
|
||||
if ($config['storage']) {
|
||||
$this->storage = \Kolab2FA\Storage\Base::factory($config['storage'], $config['storage_config']);
|
||||
|
@ -152,6 +183,18 @@ abstract class Base
|
|||
return $secret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the default label based on the method
|
||||
*/
|
||||
public function default_label()
|
||||
{
|
||||
if (class_exists('\\rcmail', false)) {
|
||||
return \rcmail::get_instance()->gettext($this->method, 'kolab_2fa');
|
||||
}
|
||||
|
||||
return strtoupper($this->method);
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for read-only access to driver properties
|
||||
*/
|
||||
|
@ -170,7 +213,7 @@ abstract class Base
|
|||
if (is_callable($func)) {
|
||||
$value = call_user_func($func);
|
||||
}
|
||||
if (!isset($value)) {
|
||||
if (isset($value)) {
|
||||
$this->set_user_prop($key, $value);
|
||||
}
|
||||
}
|
||||
|
@ -206,6 +249,20 @@ abstract class Base
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit changes to storage
|
||||
*/
|
||||
public function commit()
|
||||
{
|
||||
if (!empty($this->user_props) && $this->storage && $this->pending_changes) {
|
||||
if ($this->storage->write($this->id, $this->user_props)) {
|
||||
$this->pending_changes = false;
|
||||
}
|
||||
}
|
||||
|
||||
return !$this->pending_changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dedicated setter for the username property
|
||||
*/
|
||||
|
@ -226,8 +283,10 @@ abstract class Base
|
|||
public function clear()
|
||||
{
|
||||
if ($this->storage) {
|
||||
$this->storage->remove($this->method);
|
||||
return $this->storage->remove($this->id);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -235,8 +294,8 @@ abstract class Base
|
|||
*/
|
||||
protected function get_user_prop($key)
|
||||
{
|
||||
if (!isset($this->user_props[$key]) && $this->storage) {
|
||||
$this->user_props = (array)$this->storage->read($this->method);
|
||||
if (!isset($this->user_props[$key]) && $this->storage && !$this->pending_changes) {
|
||||
$this->user_props = (array)$this->storage->read($this->id);
|
||||
}
|
||||
|
||||
return $this->user_props[$key];
|
||||
|
@ -247,15 +306,17 @@ abstract class Base
|
|||
*/
|
||||
protected function set_user_prop($key, $value)
|
||||
{
|
||||
$success = false;
|
||||
$success = true;
|
||||
$this->pending_changes |= ($this->user_props[$key] !== $value);
|
||||
$this->user_props[$key] = $value;
|
||||
|
||||
/*
|
||||
if ($this->user_settings[$key] && $this->storage) {
|
||||
$props = (array)$this->storage->read($this->method);
|
||||
$props = (array)$this->storage->read($this->id);
|
||||
$props[$key] = $value;
|
||||
$success = $this->storage->write($this->method, $props);
|
||||
$success = $this->storage->write($this->id, $props);
|
||||
}
|
||||
|
||||
*/
|
||||
return $success;
|
||||
}
|
||||
|
||||
|
|
|
@ -33,37 +33,30 @@ class HOTP extends Base
|
|||
'digest' => 'sha1',
|
||||
);
|
||||
|
||||
public $user_settings = array(
|
||||
'secret' => array(
|
||||
'type' => 'text',
|
||||
'private' => true,
|
||||
'label' => 'secret',
|
||||
'generator' => 'generate_secret',
|
||||
),
|
||||
'created' => array(
|
||||
'type' => 'datetime',
|
||||
'editable' => false,
|
||||
'hidden' => false,
|
||||
'label' => 'created',
|
||||
'generator' => 'time',
|
||||
),
|
||||
'counter' => array(
|
||||
'type' => 'integer',
|
||||
'editable' => false,
|
||||
'hidden' => true,
|
||||
'generator' => 'random_counter',
|
||||
),
|
||||
);
|
||||
|
||||
protected $backend;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public function init(array $config)
|
||||
public function init($config)
|
||||
{
|
||||
parent::init($config);
|
||||
|
||||
$this->user_settings += array(
|
||||
'secret' => array(
|
||||
'type' => 'text',
|
||||
'private' => true,
|
||||
'label' => 'secret',
|
||||
'generator' => 'generate_secret',
|
||||
),
|
||||
'counter' => array(
|
||||
'type' => 'integer',
|
||||
'editable' => false,
|
||||
'hidden' => true,
|
||||
'generator' => 'random_counter',
|
||||
),
|
||||
);
|
||||
|
||||
// copy config options
|
||||
$this->backend = new \Kolab2FA\OTP\HOTP();
|
||||
$this->backend
|
||||
|
@ -108,6 +101,7 @@ class HOTP extends Base
|
|||
$this->set('secret', $this->get('secret', true));
|
||||
$this->set('counter', $this->get('counter', true));
|
||||
$this->set('created', $this->get('created', true));
|
||||
$this->commit();
|
||||
}
|
||||
|
||||
// TODO: deny call if already active?
|
||||
|
|
|
@ -33,31 +33,24 @@ class TOTP extends Base
|
|||
'digest' => 'sha1',
|
||||
);
|
||||
|
||||
public $user_settings = array(
|
||||
'secret' => array(
|
||||
'type' => 'text',
|
||||
'private' => true,
|
||||
'label' => 'secret',
|
||||
'generator' => 'generate_secret',
|
||||
),
|
||||
'created' => array(
|
||||
'type' => 'datetime',
|
||||
'editable' => false,
|
||||
'hidden' => false,
|
||||
'label' => 'created',
|
||||
'generator' => 'time',
|
||||
),
|
||||
);
|
||||
|
||||
protected $backend;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public function init(array $config)
|
||||
public function init($config)
|
||||
{
|
||||
parent::init($config);
|
||||
|
||||
$this->user_settings += array(
|
||||
'secret' => array(
|
||||
'type' => 'text',
|
||||
'private' => true,
|
||||
'label' => 'secret',
|
||||
'generator' => 'generate_secret',
|
||||
),
|
||||
);
|
||||
|
||||
// copy config options
|
||||
$this->backend = new \Kolab2FA\OTP\TOTP();
|
||||
$this->backend
|
||||
|
@ -103,10 +96,13 @@ class TOTP extends Base
|
|||
*/
|
||||
public function get_provisioning_uri()
|
||||
{
|
||||
console('PROV', $this->secret);
|
||||
if (!$this->secret) {
|
||||
// generate new secret and store it
|
||||
$this->set('secret', $this->get('secret', true));
|
||||
$this->set('created', $this->get('created', true));
|
||||
console('PROV2', $this->secret);
|
||||
$this->commit();
|
||||
}
|
||||
|
||||
// TODO: deny call if already active?
|
||||
|
|
|
@ -33,21 +33,6 @@ class Yubikey extends Base
|
|||
'hosts' => null,
|
||||
);
|
||||
|
||||
public $user_settings = array(
|
||||
'yubikeyid' => array(
|
||||
'type' => 'text',
|
||||
'editable' => true,
|
||||
'label' => 'secret',
|
||||
),
|
||||
'created' => array(
|
||||
'type' => 'datetime',
|
||||
'editable' => false,
|
||||
'hidden' => false,
|
||||
'label' => 'created',
|
||||
'generator' => 'time',
|
||||
),
|
||||
);
|
||||
|
||||
protected $backend;
|
||||
|
||||
/**
|
||||
|
@ -57,6 +42,14 @@ class Yubikey extends Base
|
|||
{
|
||||
parent::init($config);
|
||||
|
||||
$this->user_settings += array(
|
||||
'yubikeyid' => array(
|
||||
'type' => 'text',
|
||||
'editable' => true,
|
||||
'label' => 'secret',
|
||||
),
|
||||
);
|
||||
|
||||
// initialize validator
|
||||
$this->backend = new \Yubikey\Validate($this->config['apikey'], $this->config['clientid']);
|
||||
|
||||
|
|
|
@ -73,6 +73,11 @@ abstract class Base
|
|||
$this->username = $username;
|
||||
}
|
||||
|
||||
/**
|
||||
* List keys holding settings for 2-factor-authentication
|
||||
*/
|
||||
abstract public function enumerate();
|
||||
|
||||
/**
|
||||
* Read data for the given key
|
||||
*/
|
||||
|
|
|
@ -30,9 +30,7 @@ class RcubeUser extends Base
|
|||
{
|
||||
// sefault config
|
||||
protected $config = array(
|
||||
'keymap' => array(
|
||||
'active' => 'kolab_2fa_factors',
|
||||
),
|
||||
'keymap' => array(),
|
||||
);
|
||||
|
||||
private $cache = array();
|
||||
|
@ -46,15 +44,29 @@ class RcubeUser extends Base
|
|||
$this->config['hostname'] = $rcmail->user->ID ? $rcmail->user->data['mail_host'] : $_SESSION['hostname'];
|
||||
}
|
||||
|
||||
/**
|
||||
* List/set methods activated for this user
|
||||
*/
|
||||
public function enumerate()
|
||||
{
|
||||
if ($factors = $this->get_factors()) {
|
||||
return array_keys(array_filter($factors, function($prop) {
|
||||
return !empty($prop['active']);
|
||||
}));
|
||||
}
|
||||
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Read data for the given key
|
||||
*/
|
||||
public function read($key)
|
||||
{
|
||||
if (!isset($this->cache[$key]) && ($user = $this->get_user($this->username))) {
|
||||
$prefs = $user->get_prefs();
|
||||
$pkey = $this->key2property($key);
|
||||
$this->cache[$key] = $prefs[$pkey];
|
||||
if (!isset($this->cache[$key])) {
|
||||
$factors = $this->get_factors();
|
||||
console('READ', $key, $factors);
|
||||
$this->cache[$key] = $factors[$key];
|
||||
}
|
||||
|
||||
return $this->cache[$key];
|
||||
|
@ -67,8 +79,37 @@ class RcubeUser extends Base
|
|||
{
|
||||
if ($user = $this->get_user($this->username)) {
|
||||
$this->cache[$key] = $value;
|
||||
$pkey = $this->key2property($key);
|
||||
return $user->save_prefs(array($pkey => $value), true);
|
||||
|
||||
$factors = $this->get_factors();
|
||||
$factors[$key] = $value;
|
||||
|
||||
$pkey = $this->key2property('blob');
|
||||
$save_data = array($pkey => $factors);
|
||||
$update_index = false;
|
||||
|
||||
// remove entry
|
||||
if ($value === null) {
|
||||
unset($factors[$key]);
|
||||
$update_index = true;
|
||||
}
|
||||
// remove non-active entries
|
||||
else if (!empty($value['active'])) {
|
||||
$factors = array_filter($factors, function($prop) {
|
||||
return !empty($prop['active']);
|
||||
});
|
||||
$update_index = true;
|
||||
}
|
||||
|
||||
// update the index of active factors
|
||||
if ($update_index) {
|
||||
$save_data[$this->key2property('factors')] = array_keys(
|
||||
array_filter($factors, function($prop) {
|
||||
return !empty($prop['active']);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return $user->save_prefs($save_data, true);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -112,6 +153,19 @@ class RcubeUser extends Base
|
|||
return $this->user;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private function get_factors()
|
||||
{
|
||||
if ($user = $this->get_user($this->username)) {
|
||||
$prefs = $user->get_prefs();
|
||||
return (array)$prefs[$this->key2property('blob')];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
|
@ -123,7 +177,7 @@ class RcubeUser extends Base
|
|||
}
|
||||
|
||||
// default
|
||||
return 'kolab_2fa_props_' . $key;
|
||||
return 'kolab_2fa_' . $key;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -23,8 +23,9 @@ $labels['yubikey'] = 'Yubikey';
|
|||
|
||||
$labels['or'] = 'or';
|
||||
$labels['yes'] = 'yes';
|
||||
$labels['now'] = 'no';
|
||||
$labels['no'] = 'no';
|
||||
|
||||
$labels['label'] = 'Name';
|
||||
$labels['qrcode'] = 'QR Code';
|
||||
$labels['showqrcode'] = 'Show QR Code';
|
||||
$labels['qrcodeexplaintotp'] = 'Download an authenticator app on your phone. Two apps which work well are <strong>FreeOTP</strong> and <strong>Google Authenticator</strong>, but any other TOTP app should also work.<br/><br/>
|
||||
|
|
Loading…
Add table
Reference in a new issue