Refactor the 2FA plugin/drivers/storage to allow multiple factors of the same type

This commit is contained in:
Thomas Bruederli 2015-06-10 18:20:08 +02:00
parent 358ac3e33f
commit 7f3a76fdad
9 changed files with 275 additions and 165 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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