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