Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F120837198
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
28 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/plugins/kolab_2fa/kolab_2fa.php b/plugins/kolab_2fa/kolab_2fa.php
index f3f85197..fb1afdc8 100644
--- a/plugins/kolab_2fa/kolab_2fa.php
+++ b/plugins/kolab_2fa/kolab_2fa.php
@@ -1,774 +1,787 @@
<?php
/**
* Kolab 2-Factor-Authentication plugin
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2015, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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'];
+ // Convert username to lowercase. Copied from rcmail::login()
+ $login_lc = $rcmail->config->get('login_lc', 2);
+ if ($login_lc) {
+ if ($login_lc == 2 || $login_lc === true) {
+ $args['user'] = mb_strtolower($args['user']);
+ }
+ else if (strpos($args['user'], '@')) {
+ // lowercase domain name
+ list($local, $domain) = explode('@', $args['user']);
+ $args['user'] = $local . '@' . mb_strtolower($domain);
+ }
+ }
+
// 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_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['factors'])) {
$factors = (array)$lookup['factors'];
}
// 2b. check storage if this user has 2FA enabled
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',
'data-icon' => 'key' // for Elastic
) + $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'])) {
$out .= html::p('formbuttons', html::tag('button', array(
'type' => 'submit',
'id' => 'rcmloginsubmit',
'class' => 'button mainaction save',
), $this->gettext('continue'))
);
}
// 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 authentication factor
*
* @param string $factor Factor identifier (<method>:<id>)
* @return Kolab2FA\Driver\Base
*/
public function get_driver($factor)
{
list($method) = explode(':', $factor, 2);
$rcmail = rcmail::get_instance();
if ($this->drivers[$factor]) {
return $this->drivers[$factor];
}
$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($factor, $config);
// attach storage
$driver->storage = $this->get_storage();
if ($rcmail->user->ID) {
$driver->username = $rcmail->get_user_name();
}
$this->drivers[$factor] = $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);
$this->storage->set_logger(new \Kolab2FA\Log\RcubeLogger());
// 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' => 'twofactorauth',
'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();
$attrib['id'] = 'kolab2fa-add';
$select = new html_select($attrib);
$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;
$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);
$this->api->output->set_env('kolab_2fa_factors', !empty($factors) ? $factors : null);
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::div('explain form-text', $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::div('explain form-text', $this->gettext("qrcodeexplain$method"))
. 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::div('explain form-text', $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',
'class' => 'propform',
),
html::tag('fieldset', array(),
html::tag('legend', array(), $this->gettext($method)) .
html::div('factorprop', $table->show()) .
$input_id->show()
)
);
}
return $out;
}
/**
* Render the high-security-dialog content
*/
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));
$label = html::label(array('for' => $field_id, 'class' => 'col-form-label col-sm-4'), '$name');
return html::div($attrib,
html::div('explain form-text', $this->gettext('highsecuritydialog'))
. html::div('propform row form-group', $label . html::div('col-sm-8', $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);
}
// commit changes to the user properties
if (!$errors) {
if ($success = $driver->commit()) {
$save_data = $data !== false ? $this->format_props($driver->props()) : array();
}
else {
$errors++;
}
}
}
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,
'id' => $driver->id) + $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
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Apr 24, 1:41 PM (1 d, 13 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18864455
Default Alt Text
(28 KB)
Attached To
Mode
rRPK roundcubemail-plugins-kolab
Attached
Detach File
Event Timeline