diff --git a/plugins/kolab_2fa/config.inc.php.dist b/plugins/kolab_2fa/config.inc.php.dist
index 003e5826..a601823b 100644
--- a/plugins/kolab_2fa/config.inc.php.dist
+++ b/plugins/kolab_2fa/config.inc.php.dist
@@ -1,82 +1,124 @@
*
* 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 .
*/
// available methods/providers. Supported methods are: 'totp','hotp','yubikey'
$config['kolab_2fa_drivers'] = array('totp');
// backend for storing 2-factor-auth related per-user settings
// available backends are: 'roundcube', 'ldap', 'sql'
$config['kolab_2fa_storage'] = 'roundcube';
// additional config options for the above storage backend
// here an example for the LDAP backend:
$config['kolab_2fa_storage_config'] = array(
+ 'debug' => false,
'hosts' => array('localhost'),
'port' => 389,
- 'bind_dn' => 'uid=kolab-service,ou=Special Users,dc=example,dc=org',
+ 'bind_dn' => 'uid=kolab-auth-service,ou=Special Users,dc=example,dc=org',
'bind_pass' => 'Welcome2KolabSystems',
- 'base_dn' => 'ou=People,dc=example,dc=org',
- 'filter' => '(&(objectClass=inetOrgPerson)(mail=%fu))',
+ 'base_dn' => 'ou=Tokens,dc=example,dc=org',
+ // filter used to list stored factors for a user
+ 'filter' => '(&(objectClass=ipaToken)(objectclass=ldapSubEntry)(ipatokenOwner=%fu))',
'scope' => 'sub',
- 'debug' => true,
+ // translates driver properties to LDAP attributes
'fieldmap' => array(
- 'active' => 'nsroledn',
- '@totp' => 'kolabAuthTOTP',
- '@hotp' => 'kolabAuthHOTP',
- '@yubikey' => 'kolabAuthYubikey',
+ 'label' => 'cn',
+ 'id' => 'ipatokenUniqueID',
+ 'active' => 'ipatokenDisabled',
+ 'created' => 'ipatokenNotBefore',
+ 'userdn' => 'ipatokenOwner',
+ 'secret' => 'ipatokenOTPkey',
+ // HOTP attributes
+ 'counter' => 'ipatokenHOTPcounter',
+ 'digest' => 'ipatokenOTPalgorithm',
+ 'digits' => 'ipatokenOTPdigits',
),
+ // LDAP object classes derived from factor IDs (prefix)
+ // will be translated into the %c placeholder
+ 'classmap' => array(
+ 'totp:' => 'ipatokenTOTP',
+ 'hotp:' => 'ipatokenHOTP',
+ '*' => 'ipaToken',
+ ),
+ // translates property values into LDAP attribute values and vice versa
'valuemap' => array(
- 'nsroledn' => array(
- 'totp' => 'cn=totp-user,dc=example,dc=org',
- 'hotp' => 'cn=hotp-user,dc=example,dc=org',
- 'yubikey' => 'cn=yubikey-user,dc=example,dc=org',
+ 'active' => array(
+ false => 'TRUE',
+ true => 'FALSE',
),
),
+ // specify non-string data types for properties for implicit conversion
+ 'attrtypes' => array(
+ 'created' => 'datetime',
+ 'counter' => 'integer',
+ 'digits' => 'integer',
+ ),
+ // apply these default values to factor records if not specified by the drivers
+ 'defaults' => array(
+ 'active' => false,
+ // these are required for ipatokenHOTP records and should match the kolab_2fa_hotp parameters
+ 'digest' => 'sha1',
+ 'digits' => 6,
+ ),
+ // use this LDAP attribute to compose DN values for factor entries
+ 'rdn' => 'ipatokenUniqueID',
+ // assign these object classes to new factor entries
+ 'objectclass' => array(
+ 'top',
+ 'ipaToken',
+ '%c',
+ 'ldapSubEntry',
+ ),
+ // add these roles to the user's LDAP record if key prefix-matches a factor entry
+ 'user_roles' => array(
+ 'totp:' => 'cn=totp-user,dc=example,dc=org',
+ 'hotp:' => 'cn=hotp-user,dc=example,dc=org',
+ ),
);
-// force a two-factor authentication for all users
+// force a lookup for active authentication factors for this user.
// to be set by another plugin (e.g. kolab_auth based on LDAP roles)
-// $config['kolab_2fa_factors'] = array('totp');
+// $config['kolab_2fa_check'] = true;
// timeout for 2nd factor auth submission (in seconds)
$config['kolab_2fa_timeout'] = 60;
// configuration parameters for TOTP (uncomment to adjust)
$config['kolab_2fa_totp'] = array(
// 'digits' => 6,
// 'interval' => 30,
// 'digest' => 'sha1',
// 'issuer' => 'Roundcube',
);
// configuration parameters for HOTP (uncomment to adjust)
$config['kolab_2fa_hotp'] = array(
// 'digits' => 6,
// 'window' => 4,
// 'digest' => 'sha1',
);
// configuration parameters for Yubikey (uncomment to adjust)
$config['kolab_2fa_yubikey'] = array(
'clientid' => '123456',
'apikey' => '',
// 'hosts' => array('api.myhost1.com','api2.myhost.com'),
);
\ No newline at end of file
diff --git a/plugins/kolab_2fa/kolab_2fa.php b/plugins/kolab_2fa/kolab_2fa.php
index 4ee7e8d8..0d33bf2d 100644
--- a/plugins/kolab_2fa/kolab_2fa.php
+++ b/plugins/kolab_2fa/kolab_2fa.php
@@ -1,746 +1,751 @@
*
* Copyright (C) 2015, Kolab Systems AG
*
* 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 .
*/
class kolab_2fa extends rcube_plugin
{
public $task = '(login|settings)';
protected $login_verified = null;
protected $login_factors = array();
protected $drivers = array();
protected $storage;
/**
* Plugin init
*/
public function init()
{
$this->load_config();
$this->add_hook('startup', array($this, 'startup'));
}
/**
* Startup hook
*/
public function startup($args)
{
$rcmail = rcmail::get_instance();
// register library namespace to autoloader
$loader = include(INSTALL_PATH . 'vendor/autoload.php');
$loader->set('Kolab2FA', array($this->home . '/lib'));
if ($args['task'] === 'login' && $this->api->output) {
$this->add_texts('localization/', false);
$this->add_hook('authenticate', array($this, 'authenticate'));
// process 2nd factor auth step after regular login
if ($args['action'] === 'plugin.kolab-2fa-login' /* || !empty($_SESSION['kolab_2fa_factors']) */) {
return $this->login_verify($args);
}
}
else if ($args['task'] === 'settings') {
$this->add_texts('localization/', !$this->api->output->ajax_call);
$this->add_hook('settings_actions', array($this, 'settings_actions'));
$this->register_action('plugin.kolab-2fa', array($this, 'settings_view'));
$this->register_action('plugin.kolab-2fa-data', array($this, 'settings_data'));
$this->register_action('plugin.kolab-2fa-save', array($this, 'settings_save'));
$this->register_action('plugin.kolab-2fa-verify', array($this, 'settings_verify'));
}
return $args;
}
/**
* Handler for 'authenticate' plugin hook.
*
* ATTENTION: needs to be called *after* kolab_auth::authenticate()
*/
public function authenticate($args)
{
// nothing to be done for me
if ($args['abort'] || $this->login_verified !== null) {
return $args;
}
$rcmail = rcmail::get_instance();
// parse $host URL
$a_host = parse_url($args['host']);
$hostname = $_SESSION['hostname'] = $a_host['host'] ?: $args['host'];
// 1. find user record (and its prefs) before IMAP login
if ($user = rcube_user::query($args['user'], $hostname)) {
$rcmail->config->set_user_prefs($user->get_prefs());
}
// 2a. let plugins provide the list of active authentication factors
- $lookup = $rcmail->plugins->exec_hook('kolab_2fa_factors', array(
- 'user' => $args['user'],
- 'host' => $hostname,
- 'active' => $rcmail->config->get('kolab_2fa_factors'),
+ $lookup = $rcmail->plugins->exec_hook('kolab_2fa_lookup', array(
+ 'user' => $args['user'],
+ 'host' => $hostname,
+ 'factors' => $rcmail->config->get('kolab_2fa_factors'),
+ 'check' => $rcmail->config->get('kolab_2fa_check', true),
));
- if (isset($lookup['active'])) {
- $factors = (array)$lookup['active'];
+ if (isset($lookup['factors'])) {
+ $factors = (array)$lookup['factors'];
}
// 2b. check storage if this user has 2FA enabled
- else if ($storage = $this->get_storage($args['user'])) {
+ else if ($lookup['check'] !== false && ($storage = $this->get_storage($args['user']))) {
$factors = (array)$storage->enumerate();
}
if (count($factors) > 0) {
$args['abort'] = true;
$factors = array_unique($factors);
// 3. flag session for 2nd factor verification
$_SESSION['kolab_2fa_time'] = time();
$_SESSION['kolab_2fa_nonce'] = bin2hex(openssl_random_pseudo_bytes(32));
$_SESSION['kolab_2fa_factors'] = $factors;
$_SESSION['username'] = $args['user'];
$_SESSION['host'] = $args['host'];
$_SESSION['password'] = $rcmail->encrypt($args['pass']);
// 4. render to 2nd auth step
$this->login_step($factors);
}
return $args;
}
/**
* Handler for the additional login step requesting the 2FA verification code
*/
public function login_step($factors)
{
// replace handler for login form
$this->login_factors = array_values($factors);
$this->api->output->add_handler('loginform', array($this, 'auth_form'));
// focus the code input field on load
$this->api->output->add_script('$("input.kolab2facode").first().select();', 'docready');
$this->api->output->send('login');
}
/**
* Process the 2nd factor code verification form submission
*/
public function login_verify($args)
{
$rcmail = rcmail::get_instance();
$time = $_SESSION['kolab_2fa_time'];
$nonce = $_SESSION['kolab_2fa_nonce'];
$factors = (array)$_SESSION['kolab_2fa_factors'];
$this->login_verified = false;
$expired = $time < time() - $rcmail->config->get('kolab_2fa_timeout', 120);
if (!empty($factors) && !empty($nonce) && !$expired) {
// TODO: check signature
// try to verify each configured factor
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($factor, $code);
// accept first successful method
if ($this->login_verified) {
break;
}
}
}
if ($this->login_verified) {
// restore POST data from session
$_POST['_user'] = $_SESSION['username'];
$_POST['_host'] = $_SESSION['host'];
$_POST['_pass'] = $rcmail->decrypt($_SESSION['password']);
}
// proceed with regular login ...
$args['action'] = 'login';
// session data will be reset in index.php thus additional
// auth attempts with intercepted data will be rejected
// $rcmail->kill_session();
// we can't display any custom error messages on failed login
// but that's actually desired to expose as little information as possible
return $args;
}
/**
* Helper method to verify the given method/code tuple
*/
protected function verify_factor_auth($method, $code)
{
if (strlen($code) && ($driver = $this->get_driver($method))) {
// set properties from login
$driver->username = $_SESSION['username'];
try {
// verify the submitted code
return $driver->verify($code, $_SESSION['kolab_2fa_time']);
}
catch (Exception $e) {
rcube::raise_error($e, true, false);
}
}
return false;
}
/**
* Render 2nd factor authentication form in place of the regular login form
*/
public function auth_form($attrib = array())
{
$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)));
// create HTML table with two cols
$table = new html_table(array('cols' => 2));
$required = count($methods) > 1 ? null : 'required';
// render input for each configured auth method
foreach ($methods as $i => $method) {
if ($row++ > 0) {
$table->add(array('colspan' => 2, 'class' => 'title hint', 'style' => 'text-align:center'),
$this->gettext('or'));
}
$field_id = "rcmlogin2fa$method";
$input_code = new html_inputfield(array('name' => "_${nonce}_${method}", 'class' => 'kolab2facode', 'id' => $field_id, 'required' => $required, 'autocomplete' => 'off')
+ $attrib);
$table->add('title', html::label($field_id, html::quote($this->gettext($method))));
$table->add('input', $input_code->show(''));
}
$out = $input_task->show();
$out .= $input_action->show();
$out .= $input_tzone->show();
$out .= $input_url->show();
$out .= $table->show();
// add submit button
if (rcube_utils::get_boolean($attrib['submit'])) {
$submit = new html_inputfield(array('type' => 'submit', 'id' => 'rcmloginsubmit',
'class' => 'button mainaction', 'value' => $this->gettext('continue')));
$out .= html::p('formbuttons', $submit->show());
}
// surround html output with a form tag
if (empty($attrib['form'])) {
$out = $this->api->output->form_tag(array('name' => $form_name, 'method' => 'post'), $out);
}
return $out;
}
/**
* Load driver class for the given method
*/
public function get_driver($method)
{
$rcmail = rcmail::get_instance();
if ($this->drivers[$method]) {
return $this->drivers[$method];
}
$config = $rcmail->config->get('kolab_2fa_' . $method, array());
// use product name as "issuer""
if (empty($config['issuer'])) {
$config['issuer'] = $rcmail->config->get('product_name');
}
try {
// TODO: use external auth service if configured
$driver = \Kolab2FA\Driver\Base::factory($method, $config);
// attach storage
$driver->storage = $this->get_storage();
- // set user properties from active session
if ($rcmail->user->ID) {
$driver->username = $rcmail->get_user_name();
}
$this->drivers[$method] = $driver;
return $driver;
}
catch (Exception $e) {
$error = strval($e);
}
rcube::raise_error(array(
'code' => 600,
'type' => 'php',
'file' => __FILE__,
'line' => __LINE__,
'message' => $error),
true, false);
return false;
}
/**
* Getter for a storage instance singleton
*/
public function get_storage($for = null)
{
if (!isset($this->storage) || (!empty($for) && $this->storage->username !== $for)) {
$rcmail = rcmail::get_instance();
-
try {
$this->storage = \Kolab2FA\Storage\Base::factory(
$rcmail->config->get('kolab_2fa_storage', 'roundcube'),
$rcmail->config->get('kolab_2fa_storage_config', array())
);
+
$this->storage->set_username($for);
+
+ // set user properties from active session
+ if (!empty($_SESSION['kolab_dn'])) {
+ $this->storage->userdn = $_SESSION['kolab_dn'];
+ }
}
catch (Exception $e) {
$this->storage = false;
rcube::raise_error(array(
'code' => 600,
'type' => 'php',
'file' => __FILE__,
'line' => __LINE__,
'message' => $error),
true, false);
}
}
return $this->storage;
}
/**
* Handler for 'settings_actions' hook
*/
public function settings_actions($args)
{
// register as settings action
$args['actions'][] = array(
'action' => 'plugin.kolab-2fa',
'class' => '2factorauth',
'label' => 'settingslist',
'title' => 'settingstitle',
'domain' => 'kolab_2fa',
);
return $args;
}
/**
* Handler for settings/plugin.kolab-2fa requests
*/
public function settings_view()
{
$this->register_handler('plugin.settingsform', array($this, 'settings_form'));
$this->register_handler('plugin.settingslist', array($this, 'settings_list'));
$this->register_handler('plugin.factoradder', array($this, 'settings_factoradder'));
$this->register_handler('plugin.highsecuritydialog', array($this, 'settings_highsecuritydialog'));
$this->include_script('kolab2fa.js');
$this->include_stylesheet($this->local_skin_path() . '/kolab2fa.css');
if ($this->check_secure_mode()) {
$this->api->output->set_env('session_secured', $_SESSION['kolab_2fa_secure_mode']);
}
$this->api->output->add_label('save','cancel');
$this->api->output->set_pagetitle($this->gettext('settingstitle'));
$this->api->output->send('kolab_2fa.config');
}
/**
* Render the menu to add another authentication factor
*/
public function settings_factoradder($attrib)
{
$rcmail = rcmail::get_instance();
$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);
}
return $select->show();
}
/**
* Render a list of active factor this user has configured
*/
public function settings_list($attrib = array())
{
$attrib['id'] = 'kolab2fa-factors';
$table = new html_table(array('cols' => 3));
$table->add_header('name', $this->gettext('factor'));
$table->add_header('created', $this->gettext('created'));
$table->add_header('actions', '');
return $table->show($attrib);
}
/**
* Render the settings form template object
*/
public function settings_form($attrib = array())
{
$rcmail = rcmail::get_instance();
$storage = $this->get_storage($rcmail->get_user_name());
$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(
$factors,
array_map(function($id) use ($me, &$env_methods) {
$props = array('id' => $id);
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;
}, $factors)
));
$this->api->output->set_env('kolab_2fa_methods', $env_methods);
return html::div(array('id' => 'kolab2fapropform'), $out);
}
/**
* Render the settings UI for the given method/driver
*/
protected function settings_factor($method, $attrib)
{
$out = '';
$rcmail = rcmail::get_instance();
$attrib += array('class' => 'propform');
if ($driver = $this->get_driver($method)) {
$table = new html_table(array('cols' => 2, 'class' => $attrib['class']));
foreach ($driver->props() as $field => $prop) {
if (!$prop['editable']) {
continue;
}
switch ($prop['type']) {
case 'boolean':
case 'checkbox':
$input = new html_checkbox(array('value' => '1'));
break;
case 'enum':
case 'select':
$input = new html_select(array('disabled' => $prop['readonly']));
$input->add(array_map(array($this, 'gettext'), $prop['options']), $prop['options']);
break;
default:
$input = new html_inputfield(array('size' => $prop['size'] ?: 30, 'disabled' => !$prop['editable']));
}
$explain_label = $field . 'explain' . $method;
$explain_html = $rcmail->text_exists($explain_label, 'kolab_2fa') ? html::p('explain', $this->gettext($explain_label)) : '';
$field_id = 'rcmk2fa' . $method . $field;
$table->add('title', html::label($field_id, $this->gettext($field)));
$table->add(null, $input->show('', array('id' => $field_id, 'name' => "_prop[$field]")) . $explain_html);
}
// add row for displaying the QR code
if (method_exists($driver, 'get_provisioning_uri')) {
$table->add('title', $this->gettext('qrcode'));
$table->add(null,
html::p('explain',
$this->gettext("qrcodeexplain$method")
) .
html::p(null,
html::tag('img', array('src' => 'data:image/gif;base64,R0lGODlhDwAPAIAAAMDAwAAAACH5BAEAAAAALAAAAAAPAA8AQAINhI+py+0Po5y02otnAQA7', 'class' => 'qrcode', 'rel' => $method))
)
);
// add row for testing the factor
$field_id = 'rcmk2faverify' . $method;
$table->add('title', html::label($field_id, $this->gettext('verifycode')));
$table->add(null,
html::tag('input', array('type' => 'text', 'name' => '_verify_code', 'id' => $field_id, 'class' => 'k2fa-verify', 'size' => 20, 'required' => true)) .
html::p('explain', $this->gettext("verifycodeexplain$method"))
);
}
$input_id = new html_hiddenfield(array('name' => '_prop[id]', 'value' => ''));
$out .= html::tag('form', array(
'method' => 'post',
'action' => '#',
'id' => 'kolab2fa-prop-' . $method,
'style' => 'display:none',
),
html::tag('fieldset', array(),
html::tag('legend', array(), $this->gettext($method)) .
html::div('factorprop', $table->show()) .
$input_id->show()
)
);
}
return $out;
}
/**
* Render th
*/
public function settings_highsecuritydialog($attrib = array())
{
$attrib += array('id' => 'kolab2fa-highsecuritydialog');
$field_id = 'rcmk2facode';
$input = new html_inputfield(array('name' => '_code', 'id' => $field_id, 'class' => 'verifycode', 'size' => 20));
return html::div($attrib,
html::p('explain', $this->gettext('highsecuritydialog')) .
html::div('propform', html::label($field_id, '$name') . $input->show(''))
);
}
/**
* Handler for settings/plugin.kolab-2fa-save requests
*/
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);
$rcmail = rcmail::get_instance();
$storage = $this->get_storage($rcmail->get_user_name());
$success = false;
$errors = 0;
$save_data = array();
if ($driver = $this->get_driver($method)) {
if ($data === false) {
if ($this->check_secure_mode()) {
// remove method from active factors and clear stored settings
$success = $driver->clear();
}
else {
$errors++;
}
}
else {
// verify the submitted code before saving
$verify_code = rcube_utils::get_input_value('_verify_code', rcube_utils::INPUT_POST);
$timestamp = intval(rcube_utils::get_input_value('_timestamp', rcube_utils::INPUT_POST));
if (!empty($verify_code)) {
if (!$driver->verify($verify_code, $timestamp)) {
$this->api->output->command('plugin.verify_response', array(
'id' => $driver->id,
'method' => $driver->method,
'success' => false,
'message' => str_replace('$method', $this->gettext($driver->method), $this->gettext('codeverificationfailed'))
));
$this->api->output->send();
}
}
foreach ($data as $prop => $value) {
if (!$driver->set($prop, $value)) {
$errors++;
}
}
$driver->set('active', true);
}
// update list of active factors for this user
if (!$errors) {
$success = $driver->commit();
$save_data = $data !== false ? $this->format_props($driver->props()) : array();
}
}
if ($success) {
$this->api->output->show_message($data === false ? $this->gettext('factorremovesuccess') : $this->gettext('factorsavesuccess'), 'confirmation');
$this->api->output->command('plugin.save_success', array('method' => $method, 'active' => $data !== false) + $save_data);
}
else if ($errors) {
$this->api->output->show_message($this->gettext('factorsaveerror'), 'error');
$this->api->output->command('plugin.reset_form', $method);
}
$this->api->output->send();
}
/**
* Handler for settings/plugin.kolab-2fa-data requests
*/
public function settings_data()
{
$method = rcube_utils::get_input_value('_method', rcube_utils::INPUT_POST);
if ($driver = $this->get_driver($method)) {
$data = array('method' => $method, 'id' => $driver->id);
foreach ($driver->props(true) as $field => $prop) {
$data[$field] = $prop['text'] ?: $prop['value'];
}
// generate QR code for provisioning URI
if (method_exists($driver, 'get_provisioning_uri')) {
try {
$uri = $driver->get_provisioning_uri();
$qr = new Endroid\QrCode\QrCode();
$qr->setText($uri)
->setSize(240)
->setPadding(10)
->setErrorCorrection('high')
->setForegroundColor(array('r' => 0, 'g' => 0, 'b' => 0, 'a' => 0))
->setBackgroundColor(array('r' => 255, 'g' => 255, 'b' => 255, 'a' => 0));
$data['qrcode'] = base64_encode($qr->get());
}
catch (Exception $e) {
rcube::raise_error($e, true, false);
}
}
$this->api->output->command('plugin.render_data', $data);
}
$this->api->output->send();
}
/**
* Handler for settings/plugin.kolab-2fa-verify requests
*/
public function settings_verify()
{
$method = rcube_utils::get_input_value('_method', rcube_utils::INPUT_POST);
$timestamp = intval(rcube_utils::get_input_value('_timestamp', rcube_utils::INPUT_POST));
$success = false;
if ($driver = $this->get_driver($method)) {
$data = @json_decode(rcube_utils::get_input_value('_data', rcube_utils::INPUT_POST), true);
if (is_array($data)) {
foreach ($data as $key => $value) {
if ($value !== '******') {
$driver->$key = $value;
}
}
}
$success = $driver->verify(rcube_utils::get_input_value('_code', rcube_utils::INPUT_POST), $timestamp);
$method = $driver->method;
}
// put session into high-security mode
if ($success && !empty($_POST['_session'])) {
$_SESSION['kolab_2fa_secure_mode'] = time();
}
$this->api->output->command('plugin.verify_response', array(
'method' => $method,
'success' => $success,
'message' => str_replace('$method', $this->gettext($method),
$this->gettext($success ? 'codeverificationpassed' : 'codeverificationfailed'))
));
$this->api->output->send();
}
/**
*
*/
protected function format_props($props)
{
$rcmail = rcmail::get_instance();
$values = array();
foreach ($props as $key => $prop) {
switch ($prop['type']) {
case 'datetime':
$value = $rcmail->format_date($prop['value']);
break;
default:
$value = $prop['value'];
}
$values[$key] = $value;
}
return $values;
}
/**
*
*/
protected function check_secure_mode()
{
$valid = ($_SESSION['kolab_2fa_secure_mode'] && $_SESSION['kolab_2fa_secure_mode'] > time() - 180);
return $valid;
}
}
\ No newline at end of file
diff --git a/plugins/kolab_2fa/lib/Kolab2FA/Driver/Base.php b/plugins/kolab_2fa/lib/Kolab2FA/Driver/Base.php
index 89488231..b6a7080a 100644
--- a/plugins/kolab_2fa/lib/Kolab2FA/Driver/Base.php
+++ b/plugins/kolab_2fa/lib/Kolab2FA/Driver/Base.php
@@ -1,352 +1,346 @@
*
* Copyright (C) 2015, Kolab Systems AG
*
* 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 .
*/
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 $temporary = false;
protected $allowed_props = array('username');
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($id, $config)
{
list($method) = explode(':', $id);
$classmap = array(
'totp' => '\\Kolab2FA\\Driver\\TOTP',
'hotp' => '\\Kolab2FA\\Driver\\HOTP',
'yubikey' => '\\Kolab2FA\\Driver\\Yubikey',
);
$cls = $classmap[strtolower($method)];
if ($cls && class_exists($cls)) {
return new $cls($config, $id);
}
throw new Exception("Unknown 2FA driver '$method'");
}
/**
* Default constructor
*/
public function __construct($config = null, $id = null)
{
$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));
+ $this->temporary = true;
}
}
/**
* Initialize the driver with the given config options
*/
public function init($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']);
}
}
/**
* Verify the submitted authentication code
*
* @param string $code The 2nd authentication factor to verify
* @param int $timestamp Timestamp of authentication process (window start)
* @return boolean True if valid, false otherwise
*/
abstract function verify($code, $timestamp = null);
/**
* Getter for user-visible properties
*/
public function props($force = false)
{
$data = array();
foreach ($this->user_settings as $key => $p) {
if ($p['private']) {
continue;
}
$data[$key] = array(
'type' => $p['type'],
'editable' => $p['editable'],
'hidden' => $p['hidden'],
'label' => $p['label'],
'value' => $this->get($key, $force),
);
// format value into text
switch ($p['type']) {
case 'boolean':
$data[$key]['value'] = (bool)$data[$key]['value'];
$data[$key]['text'] = $data[$key]['value'] ? 'yes' : 'no';
break;
case 'datetime':
if (is_numeric($data[$key]['value'])) {
$data[$key]['text'] = date('c', $data[$key]['value']);
break;
}
default:
$data[$key]['text'] = $data[$key]['value'];
}
}
return $data;
}
/**
* Implement this method if the driver can be prpvisioned via QR code
*/
/* abstract function get_provisioning_uri(); */
/**
* Generate a random secret string
*/
public function generate_secret($length = 16)
{
// Base32 characters
$chars = array(
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 7
'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 15
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 23
'Y', 'Z', '2', '3', '4', '5', '6', '7', // 31
);
$secret = '';
for ($i = 0; $i < $length; $i++) {
$secret .= $chars[array_rand($chars)];
}
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
*/
public function get($key, $force = false)
{
// this is a per-user property: get from persistent storage
if (isset($this->user_settings[$key])) {
$value = $this->get_user_prop($key);
// generate property value
if (!isset($value) && $force && $this->user_settings[$key]['generator']) {
$func = $this->user_settings[$key]['generator'];
if (is_string($func) && !is_callable($func)) {
$func = array($this, $func);
}
if (is_callable($func)) {
$value = call_user_func($func);
}
if (isset($value)) {
$this->set_user_prop($key, $value);
}
}
}
else {
$value = $this->props[$key];
}
return $value;
}
/**
* Setter for restricted access to driver properties
*/
public function set($key, $value, $persistent = true)
{
// store as per-user property
if (isset($this->user_settings[$key])) {
if ($persistent) {
return $this->set_user_prop($key, $value);
}
$this->user_props[$key] = $value;
}
$setter = 'set_' . $key;
if (method_exists($this, $setter)) {
call_user_func(array($this, $setter), $value);
}
else if (in_array($key, $this->allowed_props)) {
$this->props[$key] = $value;
}
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;
+ $this->temporary = false;
}
}
return !$this->pending_changes;
}
/**
* Dedicated setter for the username property
*/
public function set_username($username)
{
$this->props['username'] = $username;
if ($this->storage) {
$this->storage->set_username($username);
}
return true;
}
/**
* Clear data stored for this driver
*/
public function clear()
{
if ($this->storage) {
return $this->storage->remove($this->id);
}
return false;
}
/**
* Getter for per-user properties for this method
*/
protected function get_user_prop($key)
{
- if (!isset($this->user_props[$key]) && $this->storage && !$this->pending_changes) {
+ if (!isset($this->user_props[$key]) && $this->storage && !$this->pending_changes && !$this->temporary) {
$this->user_props = (array)$this->storage->read($this->id);
}
return $this->user_props[$key];
}
/**
* Setter for per-user properties for this method
*/
protected function set_user_prop($key, $value)
{
- $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->id);
- $props[$key] = $value;
- $success = $this->storage->write($this->id, $props);
- }
-*/
- return $success;
+ return true;
}
/**
* Magic getter for read-only access to driver properties
*/
public function __get($key)
{
// this is a per-user property: get from persistent storage
if (isset($this->user_settings[$key])) {
return $this->get_user_prop($key);
}
return $this->props[$key];
}
/**
* Magic setter for restricted access to driver properties
*/
public function __set($key, $value)
{
$this->set($key, $value, false);
}
/**
* Magic check if driver property is defined
*/
public function __isset($key)
{
return isset($this->props[$key]);
}
}
\ No newline at end of file
diff --git a/plugins/kolab_2fa/lib/Kolab2FA/Driver/HOTP.php b/plugins/kolab_2fa/lib/Kolab2FA/Driver/HOTP.php
index 89542242..b51e0052 100644
--- a/plugins/kolab_2fa/lib/Kolab2FA/Driver/HOTP.php
+++ b/plugins/kolab_2fa/lib/Kolab2FA/Driver/HOTP.php
@@ -1,121 +1,129 @@
*
* Copyright (C) 2015, Kolab Systems AG
*
* 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 .
*/
namespace Kolab2FA\Driver;
class HOTP extends Base
{
public $method = 'hotp';
protected $config = array(
'digits' => 6,
'window' => 4,
'digest' => 'sha1',
);
protected $backend;
/**
*
*/
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
->setDigits($this->config['digits'])
->setDigest($this->config['digest'])
->setIssuer($this->config['issuer'])
->setIssuerIncludedAsParameter(true);
}
/**
*
*/
public function verify($code, $timestamp = null)
{
// get my secret from the user storage
$secret = $this->get('secret');
$counter = $this->get('counter');
if (!strlen($secret)) {
// LOG: "no secret set for user $this->username"
console("VERIFY HOTP: no secret set for user $this->username");
return false;
}
- $this->backend->setLabel($this->username)->setSecret($secret)->setCounter($this->get('counter'));
- $pass = $this->backend->verify($code, $counter, $this->config['window']);
+ try {
+ $this->backend->setLabel($this->username)->setSecret($secret)->setCounter(intval($this->get('counter')));
+ $pass = $this->backend->verify($code, $counter, $this->config['window']);
- // store incremented counter value
- $this->set('counter', $this->backend->getCounter());
+ // store incremented counter value
+ $this->set('counter', $this->backend->getCounter());
+ $this->commit();
+ }
+ catch (\Exception $e) {
+ // LOG: exception
+ console("VERIFY HOTP: $this->id, " . strval($e));
+ $pass = false;
+ }
// console('VERIFY HOTP', $this->username, $secret, $counter, $code, $pass);
return $pass;
}
/**
*
*/
public function get_provisioning_uri()
{
if (!$this->secret) {
// generate new secret and store it
$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?
- $this->backend->setLabel($this->username)->setSecret($this->secret)->setCounter($this->get('counter'));
+ $this->backend->setLabel($this->username)->setSecret($this->secret)->setCounter(intval($this->get('counter')));
return $this->backend->getProvisioningUri();
}
/**
* Generate a random counter value
*/
public function random_counter()
{
return mt_rand(1, 999);
}
}
diff --git a/plugins/kolab_2fa/lib/Kolab2FA/Storage/Base.php b/plugins/kolab_2fa/lib/Kolab2FA/Storage/Base.php
index ba9ac8a7..f79f2d57 100644
--- a/plugins/kolab_2fa/lib/Kolab2FA/Storage/Base.php
+++ b/plugins/kolab_2fa/lib/Kolab2FA/Storage/Base.php
@@ -1,95 +1,95 @@
*
* Copyright (C) 2015, Kolab Systems AG
*
* 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 .
*/
namespace Kolab2FA\Storage;
abstract class Base
{
public $username = null;
protected $config = array();
/**
*
*/
public static function factory($backend, $config)
{
$classmap = array(
'ldap' => '\\Kolab2FA\\Storage\\LDAP',
'roundcube' => '\\Kolab2FA\\Storage\\RcubeUser',
'rcubeuser' => '\\Kolab2FA\\Storage\\RcubeUser',
);
$cls = $classmap[strtolower($backend)];
if ($cls && class_exists($cls)) {
return new $cls($config);
}
throw new Exception("Unknown storage backend '$backend'");
}
/**
* Default constructor
*/
public function __construct($config = null)
{
if (is_array($config)) {
$this->init($config);
}
}
/**
* Initialize the driver with the given config options
*/
public function init(array $config)
{
$this->config = array_merge($this->config, $config);
}
/**
* Set username to store data for
*/
public function set_username($username)
{
$this->username = $username;
}
/**
* List keys holding settings for 2-factor-authentication
*/
abstract public function enumerate();
/**
* Read data for the given key
*/
abstract public function read($key);
/**
* Save data for the given key
*/
abstract public function write($key, $value);
/**
- * Remove the data stoed for the given key
+ * Remove the data stored for the given key
*/
abstract public function remove($key);
}
diff --git a/plugins/kolab_2fa/lib/Kolab2FA/Storage/LDAP.php b/plugins/kolab_2fa/lib/Kolab2FA/Storage/LDAP.php
index c8546bb9..67871adf 100644
--- a/plugins/kolab_2fa/lib/Kolab2FA/Storage/LDAP.php
+++ b/plugins/kolab_2fa/lib/Kolab2FA/Storage/LDAP.php
@@ -1,261 +1,374 @@
*
* Copyright (C) 2015, Kolab Systems AG
*
* 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 .
*/
namespace Kolab2FA\Storage;
use \Net_LDAP3;
class LDAP extends Base
{
+ public $userdn;
+
private $cache = array();
- private $users = array();
+ private $ldapcache = array();
private $conn;
private $error;
public function init(array $config)
{
parent::init($config);
$this->conn = new Net_LDAP3($config);
$this->conn->config_set('log_hook', array($this, 'log'));
$this->conn->connect();
$bind_pass = $this->config['bind_pass'];
$bind_user = $this->config['bind_user'];
$bind_dn = $this->config['bind_dn'];
$this->ready = $this->conn->bind($bind_dn, $bind_pass);
if (!$this->ready) {
throw new Exception("LDAP storage not ready: " . $this->error);
}
}
+ /**
+ * List/set methods activated for this user
+ */
+ public function enumerate($active = true)
+ {
+ $filter = $this->parse_vars($this->config['filter'], '*');
+ $base_dn = $this->parse_vars($this->config['base_dn'], '*');
+ $scope = $this->config['scope'] ?: 'sub';
+ $ids = array();
+
+ if ($this->ready && ($result = $this->conn->search($base_dn, $filter, $scope, array($this->config['fieldmap']['id'], $this->config['fieldmap']['active'])))) {
+ foreach ($result as $dn => $entry) {
+ $rec = $this->field_mapping($dn, Net_LDAP3::normalize_entry($entry, true));
+ if (!empty($rec['id']) && ($active === null || $active == $rec['active'])) {
+ $ids[] = $rec['id'];
+ }
+ }
+ }
+
+ // TODO: cache this in memory
+
+ return $ids;
+ }
+
/**
* Read data for the given key
*/
public function read($key)
{
- if (!isset($this->cache[$key]) && ($rec = $this->get_ldap_record($this->username, $key))) {
- $pkey = '@' . $key;
- if (!empty($this->config['fieldmap'][$pkey])) {
- $rec = @json_decode($rec[$pkey], true);
- }
- else if ($this->config['fieldmap'][$key]) {
- $rec = $rec[$key];
- }
- $this->cache[$key] = $rec;
+ if (!isset($this->cache[$key])) {
+ $this->cache[$key] = $this->get_ldap_record($this->username, $key);
}
return $this->cache[$key];
}
/**
* Save data for the given key
*/
public function write($key, $value)
{
- if ($rec = $this->get_ldap_record($this->username, $key)) {
- $old_attrs = $rec['_raw'];
- $new_attrs = $old_attrs;
+ $success = false;
+ $ldap_attrs = array();
- // serialize $value into one attribute
- $pkey = '@' . $key;
- if ($attr = $this->config['fieldmap'][$pkey]) {
- $new_attrs[$attr] = $value === null ? '' : json_encode($value);
- }
- else if ($attr = $this->config['fieldmap'][$key]) {
- $new_attrs[$attr] = $this->value_mapping($attr, $value, false);
-
- // special case nsroledn: keep other roles unknown to us
- if ($attr == 'nsroledn' && is_array($this->config['valuemap'][$attr])) {
- $map = $this->config['valuemap'][$attr];
- $new_attrs[$attr] = array_merge(
- $new_attrs[$attr],
- array_filter((array)$old_attrs[$attr], function($f) use ($map) { return !in_array($f, $map); })
- );
- }
- }
- else if (is_array($value)) {
- foreach ($value as $k => $val) {
- if ($attr = $this->config['fieldmap'][$k]) {
- $new_attrs[$attr] = $this->value_mapping($attr, $value, false);
- }
+ if (is_array($value)) {
+ // add some default values
+ $value += (array)$this->config['defaults'] + array('active' => false, 'username' => $this->username, 'userdn' => $this->userdn);
+
+ foreach ($value as $k => $val) {
+ if ($attr = $this->config['fieldmap'][$k]) {
+ $ldap_attrs[$attr] = $this->value_mapping($k, $val, false);
}
}
+ }
+ else {
+ // invalid data structure
+ return false;
+ }
+
+ // update existing record
+ if ($rec = $this->get_ldap_record($this->username, $key)) {
+ $old_attrs = $rec['_raw'];
+ $new_attrs = array_merge($old_attrs, $ldap_attrs);
$result = $this->conn->modify_entry($rec['_dn'], $old_attrs, $new_attrs);
+ $success = !empty($result);
+ }
+ // insert new record
+ else if ($this->ready) {
+ $entry_dn = $this->get_entry_dn($this->username, $key);
- if (!empty($result)) {
- $this->cache[$key] = $value;
- $this->users = array();
- }
+ // add object class attribute
+ $me = $this;
+ $ldap_attrs['objectclass'] = array_map(function($cls) use ($me, $key) {
+ return $me->parse_vars($cls, $key);
+ }, (array)$this->config['objectclass']);
- return !empty($result);
+ $success = $this->conn->add_entry($entry_dn, $ldap_attrs);
}
- return false;
+ if ($success) {
+ $this->cache[$key] = $value;
+ $this->ldapcache = array();
+
+ // cleanup: remove disabled/inactive/temporary entries
+ if ($value['active']) {
+ foreach ($this->enumerate(false) as $id) {
+ if ($id != $key) {
+ $this->remove($id);
+ }
+ }
+
+ // set user roles according to active factors
+ $this->set_user_roles();
+ }
+ }
+
+ return $success;
}
/**
- * Remove the data stoed for the given key
+ * Remove the data stored for the given key
*/
public function remove($key)
{
- return $this->write($key, null);
+ if ($this->ready) {
+ $entry_dn = $this->get_entry_dn($this->username, $key);
+ $success = $this->conn->delete_entry($entry_dn);
+
+ // set user roles according to active factors
+ if ($success) {
+ $this->set_user_roles();
+ }
+
+ return $success;
+ }
+
+ return false;
}
/**
* Set username to store data for
*/
public function set_username($username)
{
parent::set_username($username);
// reset cached values
$this->cache = array();
- $this->users = array();
+ $this->ldapcache = array();
+ }
+
+ /**
+ *
+ */
+ protected function set_user_roles()
+ {
+ if (!$this->ready || !$this->userdn || empty($this->config['user_roles'])) {
+ return false;
+ }
+
+ $auth_roles = array();
+ foreach ($this->enumerate(true) as $id) {
+ foreach ($this->config['user_roles'] as $prefix => $role) {
+ if (strpos($id, $prefix) === 0) {
+ $auth_roles[] = $role;
+ }
+ }
+ }
+
+ $role_attr = $this->config['fieldmap']['roles'] ?: 'nsroledn';
+ if ($user_attrs = $this->conn->get_entry($this->userdn, array($role_attr))) {
+ $internals = array_values($this->config['user_roles']);
+ $new_attrs = $old_attrs = Net_LDAP3::normalize_entry($user_attrs);
+ $new_attrs[$role_attr] = array_merge(
+ array_unique($auth_roles),
+ array_filter((array)$old_attrs[$role_attr], function($f) use ($internals) { return !in_array($f, $internals); })
+ );
+
+ $result = $this->conn->modify_entry($this->userdn, $old_attrs, $new_attrs);
+ return !empty($result);
+ }
+
+ return false;
}
/**
* Fetches user data from LDAP addressbook
*/
protected function get_ldap_record($user, $key)
{
- $filter = $this->parse_vars($this->config['filter'], $user, $key);
- $base_dn = $this->parse_vars($this->config['base_dn'], $user, $key);
- $scope = $this->config['scope'] ?: 'sub';
+ $entry_dn = $this->get_entry_dn($user, $key);
- $cachekey = $base_dn . $filter;
- if (!isset($this->users[$cachekey])) {
- $this->users[$cachekey] = array();
+ if (!isset($this->ldapcache[$entry_dn])) {
+ $this->ldapcache[$entry_dn] = array();
- if ($this->ready && ($result = $this->conn->search($base_dn, $filter, $scope, array_values($this->config['fieldmap'])))) {
- if ($result->count() == 1) {
- $entries = $result->entries(true);
- $dn = key($entries);
- $entry = array_pop($entries);
- $this->users[$cachekey] = $this->field_mapping($dn, $entry);
- }
+ if ($this->ready && ($entry = $this->conn->get_entry($entry_dn, array_values($this->config['fieldmap'])))) {
+ $this->ldapcache[$entry_dn] = $this->field_mapping($entry_dn, Net_LDAP3::normalize_entry($entry, true));
}
}
- return $this->users[$cachekey];
+ return $this->ldapcache[$entry_dn];
+ }
+
+ /**
+ * Compose a full DN for the given record identifier
+ */
+ protected function get_entry_dn($user, $key)
+ {
+ $base_dn = $this->parse_vars($this->config['base_dn'], $key);
+ return sprintf('%s=%s,%s', $this->config['rdn'], Net_LDAP3::quote_string($key, true), $base_dn);
}
/**
* Maps LDAP attributes to defined fields
*/
protected function field_mapping($dn, $entry)
{
$entry['_dn'] = $dn;
$entry['_raw'] = $entry;
// fields mapping
foreach ($this->config['fieldmap'] as $field => $attr) {
$attr_lc = strtolower($attr);
if (isset($entry[$attr_lc])) {
- $entry[$field] = $this->value_mapping($attr_lc, $entry[$attr_lc], true);
+ $entry[$field] = $this->value_mapping($field, $entry[$attr_lc], true);
}
else if (isset($entry[$attr])) {
- $entry[$field] = $this->value_mapping($attr, $entry[$attr], true);
+ $entry[$field] = $this->value_mapping($field, $entry[$attr], true);
}
}
return $entry;
}
/**
*
*/
protected function value_mapping($attr, $value, $reverse = false)
{
if ($map = $this->config['valuemap'][$attr]) {
if ($reverse) {
$map = array_flip($map);
}
if (is_array($value)) {
$value = array_filter(array_map(function($val) use ($map) {
return $map[$val];
}, $value));
}
else {
$value = $map[$value];
}
}
+ // convert (date) type
+ switch ($this->config['attrtypes'][$attr]) {
+ case 'datetime':
+ $ts = is_numeric($value) ? $value : strtotime($value);
+ if ($ts) {
+ $value = gmdate($reverse ? 'U' : 'YmdHi\Z', $ts);
+ }
+ break;
+
+ case 'integer':
+ $value = intval($value);
+ break;
+ }
+
return $value;
}
/**
* Prepares filter query for LDAP search
*/
- protected function parse_vars($str, $user, $key)
+ protected function parse_vars($str, $key)
{
- // replace variables in filter
- list($u, $d) = explode('@', $user);
+ $user = $this->username;
+
+ if (strpos($user, '@') > 0) {
+ list($u, $d) = explode('@', $user);
+ }
+ else if ($this->userdn) {
+ $u = $this->userdn;
+ $d = trim(str_replace(',dc=', '.', substr($u, strpos($u, ',dc='))), '.');
+ }
+
+ if ($this->userdn) {
+ $user = $this->userdn;
+ }
// build hierarchal domain string
$dc = $this->conn->domain_root_dn($d);
- // map key value
- if (is_array($this->config['keymap']) && isset($this->config['keymap'][$key])) {
- $key = $this->config['keymap'][$key];
- }
+ $class = $this->config['classmap'] ? $this->config['classmap']['*'] : '*';
- // TODO: resolve $user into its DN for %udn
+ // map key to objectclass
+ if (is_array($this->config['classmap'])) {
+ foreach ($this->config['classmap'] as $k => $c) {
+ if (strpos($key, $k) === 0) {
+ $class = $c;
+ break;
+ }
+ }
+ }
- $replaces = array('%dc' => $dc, '%d' => $d, '%fu' => $user, '%u' => $u, '%k' => $key);
+ $replaces = array('%dc' => $dc, '%d' => $d, '%fu' => $user, '%u' => $u, '%c' => $class);
return strtr($str, $replaces);
}
/**
* Prints debug/error info to the log
*/
public function log($level, $msg)
{
$msg = implode("\n", $msg);
switch ($level) {
case LOG_DEBUG:
case LOG_INFO:
case LOG_NOTICE:
if ($this->config['debug'] && class_exists('\\rcube', false)) {
\rcube::write_log('ldap', $msg);
}
break;
case LOG_EMERGE:
case LOG_ALERT:
case LOG_CRIT:
case LOG_ERR:
case LOG_WARNING:
$this->error = $msg;
// throw new Exception("LDAP storage error: " . $msg);
break;
}
}
}
diff --git a/plugins/kolab_2fa/lib/Kolab2FA/Storage/RcubeUser.php b/plugins/kolab_2fa/lib/Kolab2FA/Storage/RcubeUser.php
index f0664bb1..5f614704 100644
--- a/plugins/kolab_2fa/lib/Kolab2FA/Storage/RcubeUser.php
+++ b/plugins/kolab_2fa/lib/Kolab2FA/Storage/RcubeUser.php
@@ -1,183 +1,183 @@
*
* Copyright (C) 2015, Kolab Systems AG
*
* 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 .
*/
namespace Kolab2FA\Storage;
use \rcmail;
use \rcube_user;
class RcubeUser extends Base
{
// sefault config
protected $config = array(
'keymap' => array(),
);
private $cache = array();
private $user;
public function init(array $config)
{
parent::init($config);
$rcmail = rcmail::get_instance();
$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])) {
$factors = $this->get_factors();
console('READ', $key, $factors);
$this->cache[$key] = $factors[$key];
}
return $this->cache[$key];
}
/**
* Save data for the given key
*/
public function write($key, $value)
{
if ($user = $this->get_user($this->username)) {
$this->cache[$key] = $value;
$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;
}
/**
- * Remove the data stoed for the given key
+ * Remove the data stored for the given key
*/
public function remove($key)
{
return $this->write($key, null);
}
/**
* Set username to store data for
*/
public function set_username($username)
{
parent::set_username($username);
// reset cached values
$this->cache = array();
$this->user = null;
}
/**
* Helper method to get a rcube_user instance for storing prefs
*/
private function get_user($username)
{
// use global instance if we have a valid Roundcube session
$rcmail = rcmail::get_instance();
if ($rcmail->user->ID && $rcmail->user->get_username() == $username) {
return $rcmail->user;
}
if (!$this->user) {
$this->user = rcube_user::query($username, $this->config['hostname']);
}
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;
}
/**
*
*/
private function key2property($key)
{
// map key to configured property name
if (is_array($this->config['keymap']) && isset($this->config['keymap'][$key])) {
return $this->config['keymap'][$key];
}
// default
return 'kolab_2fa_' . $key;
}
}