Aleksander Machniak
2015-08-06 1b8ca08e5b042035b096c61d094fab0157941f17
Added GSSAPI/Kerberos authentication plugin - krb_authentication
3 files added
3 files modified
220 ■■■■■ changed files
CHANGELOG 1 ●●●● patch | view | raw | blame | history
plugins/krb_authentication/config.inc.php.dist 13 ●●●●● patch | view | raw | blame | history
plugins/krb_authentication/krb_authentication.php 110 ●●●●● patch | view | raw | blame | history
plugins/krb_authentication/tests/KrbAuthentication.php 23 ●●●●● patch | view | raw | blame | history
program/lib/Roundcube/rcube_imap_generic.php 72 ●●●●● patch | view | raw | blame | history
tests/phpunit.xml 1 ●●●● patch | view | raw | blame | history
CHANGELOG
@@ -1,6 +1,7 @@
CHANGELOG Roundcube Webmail
===========================
- Added GSSAPI/Kerberos authentication plugin - krb_authentication
- Password: Allow temporarily disabling the plugin functionality with a notice
- Support more secure hashing algorithms for auth cookie - configurable by PHP's session.hash_function (#1490403)
- Require Mbstring and OpenSSL extensions (#1490415)
plugins/krb_authentication/config.inc.php.dist
New file
@@ -0,0 +1,13 @@
<?php
// Kerberos/GSSAPI Authentication Plugin options
// ---------------------------------------------
// Default mail host to log-in using user/password from HTTP Authentication.
// This is useful if the users are free to choose arbitrary mail hosts (or
// from a list), but have one host they usually want to log into.
// Unlike $config['default_host'] this must be a string!
$config['krb_authentication_host'] = '';
// GSS API security context
$config['krb_authentication_context'] = 'imap/kolab.example.org@EXAMPLE.ORG';
plugins/krb_authentication/krb_authentication.php
New file
@@ -0,0 +1,110 @@
<?php
/**
 * Kerberos Authentication
 *
 * Make use of an existing Kerberos authentication and perform login
 * with the existing user credentials
 *
 * For other configuration options, see config.inc.php.dist!
 *
 * @version @package_version@
 * @license GNU GPLv3+
 * @author Jeroen van Meeuwen
 */
class krb_authentication extends rcube_plugin
{
    private $redirect_query;
    /**
     * Plugin initialization
     */
    function init()
    {
        $this->add_hook('startup', array($this, 'startup'));
        $this->add_hook('authenticate', array($this, 'authenticate'));
        $this->add_hook('login_after', array($this, 'login'));
        $this->add_hook('storage_connect', array($this, 'storage_connect'));
    }
    /**
     * Startup hook handler
     */
    function startup($args)
    {
        if (!empty($_SERVER['REMOTE_USER']) && !empty($_SERVER['KRB5CCNAME'])) {
            // handle login action
            if (empty($_SESSION['user_id'])) {
                $args['action']       = 'login';
                $this->redirect_query = $_SERVER['QUERY_STRING'];
            }
            else {
                $_SESSION['password'] = null;
            }
        }
        return $args;
    }
    /**
     * Authenticate hook handler
     */
    function authenticate($args)
    {
        if (!empty($_SERVER['REMOTE_USER']) && !empty($_SERVER['KRB5CCNAME'])) {
            // Load plugin's config file
            $this->load_config();
            $rcmail = rcmail::get_instance();
            $host   = $rcmail->config->get('krb_authentication_host');
            if (is_string($host) && trim($host) !== '' && empty($args['host'])) {
                $args['host'] = rcube_utils::idn_to_ascii(rcube_utils::parse_host($host));
            }
            if (!empty($_SERVER['REMOTE_USER'])) {
                $args['user'] = $_SERVER['REMOTE_USER'];
                $args['pass'] = null;
            }
            $args['cookiecheck'] = false;
            $args['valid']       = true;
        }
        return $args;
    }
    /**
     * Storage_connect hook handler
     */
    function storage_connect($args)
    {
        if (!empty($_SERVER['REMOTE_USER']) && !empty($_SERVER['KRB5CCNAME'])) {
            // Load plugin's config file
            $this->load_config();
            $rcmail  = rcmail::get_instance();
            $context = $rcmail->config->get('krb_authentication_context');
            $args['gssapi_context'] = $context ?: 'imap/kolab.example.org@EXAMPLE.ORG';
            $args['gssapi_cn']      = $_SERVER['KRB5CCNAME'];
            $args['auth_type']      = 'GSSAPI';
        }
        return $args;
    }
    /**
     * login_after hook handler
     */
    function login($args)
    {
        // Redirect to the previous QUERY_STRING
        if ($this->redirect_query) {
            header('Location: ./?' . $this->redirect_query);
            exit;
        }
        return $args;
    }
}
plugins/krb_authentication/tests/KrbAuthentication.php
New file
@@ -0,0 +1,23 @@
<?php
class KrbAuthentication_Plugin extends PHPUnit_Framework_TestCase
{
    function setUp()
    {
        include_once dirname(__FILE__) . '/../krb_authentication.php';
    }
    /**
     * Plugin object construction test
     */
    function test_constructor()
    {
        $rcube  = rcube::get_instance();
        $plugin = new krb_authentication($rcube->api);
        $this->assertInstanceOf('krb_authentication', $plugin);
        $this->assertInstanceOf('rcube_plugin', $plugin);
    }
}
program/lib/Roundcube/rcube_imap_generic.php
@@ -552,14 +552,12 @@
                $this->putLine($reply, true, true);
                $line = trim($this->readReply());
                if ($line[0] == '+') {
                    $challenge = substr($line, 2);
                }
                else {
                if ($line[0] != '+') {
                    return $this->parseResult($line);
                }
                // check response
                $challenge = substr($line, 2);
                $challenge = base64_decode($challenge);
                if (strpos($challenge, 'rspauth=') === false) {
                    $this->setError(self::ERROR_BAD,
@@ -571,6 +569,66 @@
            }
            $line = $this->readReply();
            $result = $this->parseResult($line);
        }
        elseif ($type == 'GSSAPI') {
            if (!extension_loaded('krb5')) {
                $this->setError(self::ERROR_BYE,
                    "The krb5 extension is required for GSSAPI authentication");
                return self::ERROR_BAD;
            }
            if (empty($this->prefs['gssapi_cn'])) {
                $this->setError(self::ERROR_BYE,
                    "The gssapi_cn parameter is required for GSSAPI authentication");
                return self::ERROR_BAD;
            }
            if (empty($this->prefs['gssapi_context'])) {
                $this->setError(self::ERROR_BYE,
                    "The gssapi_context parameter is required for GSSAPI authentication");
                return self::ERROR_BAD;
            }
            putenv('KRB5CCNAME=' . $this->prefs['gssapi_cn']);
            try {
                $ccache = new KRB5CCache();
                $ccache->open($this->prefs['gssapi_cn']);
                $gssapicontext = new GSSAPIContext();
                $gssapicontext->acquireCredentials($ccache);
                $token   = '';
                $success = $gssapicontext->initSecContext($this->prefs['gssapi_context'], null, null, null, $token);
                $token   = base64_encode($token);
            }
            catch (Exception $e) {
                trigger_error($e->getMessage(), E_USER_WARNING);
                $this->setError(self::ERROR_BYE, "GSSAPI authentication failed");
                return self::ERROR_BAD;
            }
            $this->putLine($this->nextTag() . " AUTHENTICATE GSSAPI " . $token);
            $line = trim($this->readReply());
            if ($line[0] != '+') {
                return $this->parseResult($line);
            }
            try {
                $challenge = base64_decode(substr($line, 2));
                $gssapicontext->unwrap($challenge, $challenge);
                $gssapicontext->wrap($challenge, $challenge, true);
            }
            catch (Exception $e) {
                trigger_error($e->getMessage(), E_USER_WARNING);
                $this->setError(self::ERROR_BYE, "GSSAPI authentication failed");
                return self::ERROR_BAD;
            }
            $this->putLine(base64_encode($challenge));
            $line   = $this->readReply();
            $result = $this->parseResult($line);
        }
        else { // PLAIN
@@ -738,7 +796,7 @@
            return false;
        }
        if (empty($password)) {
        if (empty($password) && empty($options['gssapi_cn'])) {
            $this->setError(self::ERROR_NO, "Empty password");
            return false;
        }
@@ -774,7 +832,8 @@
            }
            // Use best (for security) supported authentication method
            foreach (array('DIGEST-MD5', 'CRAM-MD5', 'CRAM_MD5', 'PLAIN', 'LOGIN') as $auth_method) {
            $all_methods = array('GSSAPI', 'DIGEST-MD5', 'CRAM-MD5', 'CRAM_MD5', 'PLAIN', 'LOGIN');
            foreach ($all_methods as $auth_method) {
                if (in_array($auth_method, $auth_methods)) {
                    break;
                }
@@ -803,6 +862,7 @@
            case 'CRAM-MD5':
            case 'DIGEST-MD5':
            case 'PLAIN':
            case 'GSSAPI':
                $result = $this->authenticate($user, $password, $auth_method);
                break;
            case 'LOGIN':
tests/phpunit.xml
@@ -64,6 +64,7 @@
            <file>./../plugins/http_authentication/tests/HttpAuthentication.php</file>
            <file>./../plugins/identity_select/tests/IdentitySelect.php</file>
            <file>./../plugins/jqueryui/tests/Jqueryui.php</file>
            <file>./../plugins/krb_authentication/tests/KrbAuthentication.php</file>
            <file>./../plugins/legacy_browser/tests/LegacyBrowser.php</file>
            <file>./../plugins/managesieve/tests/Managesieve.php</file>
            <file>./../plugins/managesieve/tests/Parser.php</file>