diff --git a/CHANGELOG b/CHANGELOG
index 43dd5d0a5..ba56486f0 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 000000000..63db16943
--- /dev/null
+++ b/plugins/krb_authentication/config.inc.php.dist
@@ -0,0 +1,13 @@
+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 000000000..f28f5be9d
--- /dev/null
+++ b/plugins/krb_authentication/tests/KrbAuthentication.php
@@ -0,0 +1,23 @@
+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 498793e9d..daf0abea8 100644
--- a/program/lib/Roundcube/rcube_imap_generic.php
+++ b/program/lib/Roundcube/rcube_imap_generic.php
@@ -552,14 +552,12 @@ class rcube_imap_generic
$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,
@@ -573,6 +571,66 @@ class rcube_imap_generic
$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
// proxy authorization
if (!empty($this->prefs['auth_cid'])) {
@@ -738,7 +796,7 @@ class rcube_imap_generic
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 @@ class rcube_imap_generic
}
// 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 @@ class rcube_imap_generic
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 5c27d0e0d..f5cac92f6 100644
--- a/tests/phpunit.xml
+++ b/tests/phpunit.xml
@@ -64,6 +64,7 @@
./../plugins/http_authentication/tests/HttpAuthentication.php
./../plugins/identity_select/tests/IdentitySelect.php
./../plugins/jqueryui/tests/Jqueryui.php
+ ./../plugins/krb_authentication/tests/KrbAuthentication.php
./../plugins/legacy_browser/tests/LegacyBrowser.php
./../plugins/managesieve/tests/Managesieve.php
./../plugins/managesieve/tests/Parser.php