diff --git a/plugins/kolab_sso/README b/plugins/kolab_sso/README index 3f0ad560..0c130f3d 100644 --- a/plugins/kolab_sso/README +++ b/plugins/kolab_sso/README @@ -1,65 +1,67 @@ Single Sign On Authentication for Kolab --------------------------------------- This plugin adds possibility to authenticate users via external authentication services. Currently the only supported method of authentication is OpenID Connect. Because Kolab backends do not support token authentication it is required to use master user (sasl proxy) authentication where possible and service user credentials in other places. See IMPLEMENTATION NOTES below for details. Plugin requires libkolab plugin and HTTP/Request2 library. Plugin contains BSD-licensed https://github.com/firebase/php-jwt (v5.0.0) library. Supported algorithms: - HS256, HS385, HS512 (PHP hash extension required) - RS256, RS384, RS512 (PHP openssl extension required). INSTALLATION ------------ Enable plugin in Roundcube's main configuration file. Make sure it is first on the list of plugins, especially before any authentication plugin, e.g. kolab_auth. Default return-URL for Auth Provider is https://host.roundcube?_task=login&_action=sso, but not all providers support query params. To workaround this limitation you have to define an alias URI or redirect. For example: RewriteEngine On RewriteCond %{REQUEST_URI} ^/roundcubemail RewriteRule "^sso" "/roundcubemail/?_task=login&_action=sso" [L,QSA] For the above "alias" plugin configuration should include 'response_uri' = '/sso' and on the provider side configured URI will be https://host/roundcubemail/sso. IMPLEMENTATION NOTES -------------------- [IMAP] Because Kolab backend do not support token authentication it is required to use master user (sasl proxy) authentication, i.e. you have to put master user credentials in plugin's config. [SMTP] For the same reason and also because the same master user does not work in Postfix, you have to specify SMTP connection parameters/user+password. +[LDAP] Global addressbook (read-only) requires LDAP user/password. 'user_specific' option does +not work as well as proxy authentication. + [Freebusy] Authentication into kolab-freebusy service is not yet implemented. A solution for now is to add Roundcube host(s) into trustednetworks.allow option in kolab-freebusy service config. [Chwala] Authentication to Chwala will work if it uses the same (session) database as Roundcube. Additionally set $config['fileapi_plugins'] = array('kolab_sso', 'kolab_auth', 'kolab_folders'); Authenticating to Seafile/WebDAV storage is not supported (until it's custom user-defined -storage with saved password). +storage). TODO ---- -- LDAP addressbook -- kolab_delegation (LDAP auth) +- kolab_delegation (LDAP auth for write operation) - Chwala+Seafile - Chwala+WebDAV - Freebusy auth - Mattermost auth - Extended token validation diff --git a/plugins/kolab_sso/config.inc.php.dist b/plugins/kolab_sso/config.inc.php.dist index 750919ec..fa5d59d9 100644 --- a/plugins/kolab_sso/config.inc.php.dist +++ b/plugins/kolab_sso/config.inc.php.dist @@ -1,54 +1,60 @@ array( // User-friendly name (will be displayed in the button label) 'name' => 'OpenIDC Test', // Driver name 'driver' => 'openidc', // Provider API URI 'uri' => 'https://kolab.eu.auth0.com', // Client ID/Secret for the API 'client_id' => '20w6DXX69isNBaufCwyK24wkBHqPT2ht', 'client_secret' => 'd78McGW4UWfFyZprGd8BCKooll', // Token URI, if different than /token 'token_uri' => 'https://kolab.eu.auth0.com/oauth/token', // Authorize URI, if different than /authorize 'authorize_uri' => 'https://kolab.eu.auth0.com/authorize', // Response URI, by default we use https://domain.tld/path?_task=login&_action=sso // Define it if the Provider does not allow above // to use https://domain.tld/path/ instead 'response_uri' => '/sso', // Public key for token validation when using RS256/RS385/RS512 method 'pubkey' => '-----BEGIN CERTIFICATE----- ... -----END CERTIFICATE----- ', ), ); */ diff --git a/plugins/kolab_sso/kolab_sso.php b/plugins/kolab_sso/kolab_sso.php index 22ed5ac8..8542f59c 100644 --- a/plugins/kolab_sso/kolab_sso.php +++ b/plugins/kolab_sso/kolab_sso.php @@ -1,366 +1,384 @@ * * Copyright (C) 2018, 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_sso extends rcube_plugin { public $rc; private $data; private $driver; private $debug = false; /** * Plugin initialization */ public function init() { // Roundcube or Chwala if (defined('RCMAIL_VERSION') || defined('FILE_API_START')) { $this->rc = rcube::get_instance(); $this->add_hook('startup', array($this, 'startup')); $this->add_hook('authenticate', array($this, 'authenticate')); $this->rc->add_shutdown_function(array($this, 'shutdown')); } } /** * Startup hook handler */ public function startup($args) { // On login or logout (or when the session expired)... if ($args['task'] == 'login' || $args['task'] == 'logout') { $mode = $args['action'] == 'sso' ? $_SESSION['sso_mode'] : rcube_utils::get_input_value('_sso', rcube_utils::INPUT_GP); // Authorization if ($mode) { $driver = $this->get_driver($mode); // This is where we handle redirections from the SSO provider if ($args['action'] == 'sso') { $this->data = $driver->response(); if (!empty($this->data)) { $this->data['timezone'] = $_SESSION['sso_timezone']; $this->data['url'] = $_SESSION['sso_url']; $this->data['mode'] = $mode; } } // This is where we handle clicking one of "Login by SSO" buttons else if ($_SESSION['temp'] && $this->rc->check_request()) { // Remember some logon params for use on SSO response above $_SESSION['sso_timezone'] = rcube_utils::get_input_value('_timezone', rcube_utils::INPUT_POST); $_SESSION['sso_url'] = rcube_utils::get_input_value('_url', rcube_utils::INPUT_POST); $_SESSION['sso_mode'] = $mode; $driver->authorize(); } $args['action'] = 'login'; $args['task'] = 'login'; } } // On valid session... else if (isset($_SESSION['user_id']) && ($data = $_SESSION['sso_data']) && ($data = json_decode($this->rc->decrypt($data), true)) && ($mode = $data['mode']) ) { $driver = $this->get_driver($mode); // Session validation, token refresh, etc. if ($this->data = $driver->validate_session($data)) { // register storage connection hooks $this->authenticate(array(), true); } else { // Destroy the session $this->rc->kill_session(); // TODO: error message beter explaining the reason // $this->rc->output->show_message('sessionferror', 'error'); } } // Register login form modifications $this->add_hook('template_object_loginform', array($this, 'login_form')); return $args; } /** * Authenticate hook handler */ public function authenticate($args, $internal = false) { // Chwala if (defined('FILE_API_START') && !$internal && empty($args['pass']) && strpos($args['user'], 'RC:') === 0) { // extract session ID and username from the token list(, $sess_id, $user) = explode(':', $args['user']); // unset user, set invalid state $args['valid'] = false; $args['user'] = null; $session = rcube_session::factory($this->rc->config); if ($data = $session->read($sess_id)) { // get SSO data from the existing session $old_session = $_SESSION; session_decode($data); $session_user = $_SESSION['username']; $data = $_SESSION['sso_data']; $_SESSION = $old_session; // TODO: allow only configured REMOTE_ADDR? if ($session_user == $user && $data && ($data = json_decode($this->rc->decrypt($data), true)) && ($mode = $data['mode'])) { $driver = $this->get_driver($mode); // Session validation, token refresh, etc. if ($this->data = $driver->validate_session($data)) { $args['user'] = $user; $args['pass'] = 'fake-sso-password'; $args['valid'] = true; $this->authenticate(array(), true); } } } } // Roundcube else if (!empty($this->data) && ($email = $this->data['email'])) { if (!$internal) { $args['user'] = $email; $args['pass'] = 'fake-sso-password'; $args['valid'] = true; $args['cookiecheck'] = false; $_POST['_timezone'] = $this->data['timezone']; $_POST['_url'] = $this->data['url']; } $this->add_hook('storage_connect', array($this, 'storage_connect')); $this->add_hook('managesieve_connect', array($this, 'storage_connect')); $this->add_hook('smtp_connect', array($this, 'smtp_connect')); + $this->add_hook('ldap_connected', array($this, 'ldap_connected')); $this->add_hook('chwala_authenticate', array($this, 'chwala_authenticate')); } return $args; } /** * Shutdown handler */ public function shutdown() { // Between startup and authenticate the session is destroyed. // So, we save the data later than that. if (!empty($this->data) && !empty($_SESSION['user_id'])) { $_SESSION['sso_data'] = $this->rc->encrypt(json_encode($this->data)); } } /** * Storage_connect/managesieve_connect hook handler */ public function storage_connect($args) { - $user = $this->rc->config->get('kolab_sso_username'); - $pass = $this->rc->config->get('kolab_sso_password'); + $user = $this->rc->config->get('kolab_sso_imap_user'); + $pass = $this->rc->config->get('kolab_sso_imap_pass'); if ($user && $pass) { $args['auth_cid'] = $user; $args['auth_pw'] = $pass; $args['auth_type'] = 'PLAIN'; } return $args; } /** * Smtp_connect hook handler */ public function smtp_connect($args) { foreach (array('smtp_server', 'smtp_user', 'smtp_pass') as $prop) { $args[$prop] = $this->rc->config->get("kolab_sso_$prop", $args[$prop]); } return $args; } + /** + * ldap_connected hook handler + */ + public function ldap_connected($args) + { + $user = $this->rc->config->get('kolab_sso_ldap_user'); + $pass = $this->rc->config->get('kolab_sso_ldap_pass'); + + if ($user && $pass && $args['user_specific']) { + $args['bind_dn'] = $user; + $args['bind_pass'] = $pass; + $args['search_filter'] = null; + } + + return $args; + } + /** * Chwala_authenticate hook handler */ public function chwala_authenticate($args) { // Instead of normal basic auth with user/pass we'll use // Authorization: Bearer $bearer = 'RC:' . session_id() . ':' . $_SESSION['username']; $args['request']->setAuth(null); $args['request']->setHeader('Authorization', 'Bearer ' . base64_encode($bearer)); return $args; } /** * Login form object */ public function login_form($args) { $this->load_config(); $options = (array) $this->rc->config->get('kolab_sso_options'); $disable_login = $this->rc->config->get('kolab_sso_disable_login'); if (empty($options)) { return $args; } $doc = new DOMDocumentHelper('1.0'); $doc->loadHTML($args['content']); $body = $doc->getElementsByTagName('body')->item(0); if ($disable_login) { // Remove login form inputs table $table = $doc->getElementsByTagName('table')->item(0); $table->parentNode->removeChild($table); // Remove original Submit button $submit = $doc->getElementsByTagName('button')->item(0); $submit->parentNode->removeChild($submit); } if (!$this->driver) { $this->add_texts('localization/'); } // Add SSO form elements $form = $doc->createNode('p', null, array('id' => 'sso-form', 'class' => 'formbuttons'), $body); foreach ($options as $idx => $option) { $label = array('name' => 'loginby', 'vars' => array('provider' => $option['name'] ?: $this->gettext('sso'))); $doc->createNode('button', $this->gettext($label), array( 'type' => 'button', 'value' => $idx, 'class' => 'button sso w-100', 'onclick' => 'kolab_sso_submit(this)', ), $form); } $doc->createNode('input', null, array('name' => '_sso', 'type' => 'hidden'), $form); // Save the form content back $args['content'] = preg_replace('||', '', $doc->saveHTML($body)); // Add script $args['content'] .= ""; return $args; } /** * Debug function for drivers */ public function debug($line) { if ($this->debug) { rcube::write_log('sso', $line); } } /** * Initialize SSO driver object */ private function get_driver($name) { if ($this->driver) { return $this->driver; } $this->load_config(); $this->add_texts('localization/'); $options = (array) $this->rc->config->get('kolab_sso_options'); $options = (array) $options[$name]; $driver = $options['driver'] ?: 'openidc'; $class = "kolab_sso_$driver"; if (empty($options) || !file_exists($this->home . "/drivers/$driver.php")) { rcube::raise_error(array( 'line' => __LINE__, 'file' => __FILE__, 'message' => "Unable to find SSO driver" ), true, true); } // Add /lib to include_path $include_path = $this->home . '/lib' . PATH_SEPARATOR; $include_path .= ini_get('include_path'); set_include_path($include_path); require_once $this->home . "/drivers/$driver.php"; $this->debug = $this->rc->config->get('kolab_sso_debug'); $this->driver = new $class($this, $options); return $this->driver; } } /** * DOMDocument wrapper with some shortcut method */ class DOMDocumentHelper extends DOMDocument { public function createNode($name, $value = null, $args = array(), $parent = null, $prepend = false) { $node = parent::createElement($name); if ($value) { $node->appendChild(new DOMText(rcube::Q($value))); } foreach ($args as $attr_name => $attr_value) { $node->setAttribute($attr_name, $attr_value); } if ($parent) { if ($prepend && $parent->firstChild) { $parent->insertBefore($node, $parent->firstChild); } else { $parent->appendChild($node); } } return $node; } }