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