406 lines
13 KiB
PHP
406 lines
13 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Single Sign On Authentication for Kolab
|
|
*
|
|
* @author Aleksander Machniak <machniak@kolabsys.com>
|
|
*
|
|
* Copyright (C) 2018, 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_sso extends rcube_plugin
|
|
{
|
|
public $rc;
|
|
|
|
private $data;
|
|
private $old_data;
|
|
private $driver;
|
|
private $logon_error;
|
|
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;
|
|
}
|
|
else {
|
|
$this->logon_error = $driver->response_error();
|
|
}
|
|
}
|
|
// 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);
|
|
|
|
$this->old_data = $data;
|
|
|
|
// 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'));
|
|
}
|
|
else if ($this->logon_error) {
|
|
$args['valid'] = false;
|
|
$args['error'] = $this->logon_error;
|
|
}
|
|
|
|
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'])
|
|
// update session only when data changed
|
|
&& (empty($this->old_data) || $this->old_data != $this->data)
|
|
) {
|
|
$_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_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 <roundcube session id>
|
|
$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 mb-1',
|
|
'onclick' => 'kolab_sso_submit(this)',
|
|
), $form);
|
|
}
|
|
|
|
$doc->createNode('input', null, array('name' => '_sso', 'type' => 'hidden'), $form);
|
|
|
|
// Save the form content back and append script
|
|
$args['content'] = $doc->saveHTML($body)
|
|
. "<script>"
|
|
. "function kolab_sso_submit(button) {"
|
|
. "\$('[name=_sso]').val(button.value);"
|
|
. "\$('input[type=text],input[type=password]').attr('required', false);"
|
|
. "rcmail.gui_objects.loginform.submit();"
|
|
. "}"
|
|
. "</script>";
|
|
|
|
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 loadHTML($html, $options = 0)
|
|
{
|
|
return parent::loadHTML('<html><head><meta content="text/html; charset=utf-8" http-equiv="Content-Type"></head><body>' . $html);
|
|
}
|
|
|
|
public function saveHTML($node = null)
|
|
{
|
|
return preg_replace('|</?body>|', '', parent::saveHTML($node));
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|