From 1b8ca08e5b042035b096c61d094fab0157941f17 Mon Sep 17 00:00:00 2001
From: Aleksander Machniak <alec@alec.pl>
Date: Thu, 06 Aug 2015 07:23:50 -0400
Subject: [PATCH] Added GSSAPI/Kerberos authentication plugin - krb_authentication

---
 plugins/krb_authentication/config.inc.php.dist         |   13 +++
 CHANGELOG                                              |    1 
 tests/phpunit.xml                                      |    1 
 plugins/krb_authentication/krb_authentication.php      |  110 +++++++++++++++++++++++++++
 plugins/krb_authentication/tests/KrbAuthentication.php |   23 +++++
 program/lib/Roundcube/rcube_imap_generic.php           |   72 ++++++++++++++++-
 6 files changed, 214 insertions(+), 6 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG
index 43dd5d0..ba56486 100644
--- a/CHANGELOG
+++ b/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)
diff --git a/plugins/krb_authentication/config.inc.php.dist b/plugins/krb_authentication/config.inc.php.dist
new file mode 100644
index 0000000..63db169
--- /dev/null
+++ b/plugins/krb_authentication/config.inc.php.dist
@@ -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';
diff --git a/plugins/krb_authentication/krb_authentication.php b/plugins/krb_authentication/krb_authentication.php
new file mode 100644
index 0000000..fe6f843
--- /dev/null
+++ b/plugins/krb_authentication/krb_authentication.php
@@ -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;
+    }
+}
diff --git a/plugins/krb_authentication/tests/KrbAuthentication.php b/plugins/krb_authentication/tests/KrbAuthentication.php
new file mode 100644
index 0000000..f28f5be
--- /dev/null
+++ b/plugins/krb_authentication/tests/KrbAuthentication.php
@@ -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);
+    }
+}
+
diff --git a/program/lib/Roundcube/rcube_imap_generic.php b/program/lib/Roundcube/rcube_imap_generic.php
index 498793e..daf0abe 100644
--- a/program/lib/Roundcube/rcube_imap_generic.php
+++ b/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':
diff --git a/tests/phpunit.xml b/tests/phpunit.xml
index 5c27d0e..f5cac92 100644
--- a/tests/phpunit.xml
+++ b/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>

--
Gitblit v1.9.1