Closed rosali closed 8 years ago
Dovecot provides an SASL auth service that is typically used by Postfix. I have extended SabreDAV such that it uses this SASL auth service, too.
Dovecot SASL auth class (not SabreDAV-specific):
<?php
/**
* http://wiki.dovecot.org/Authentication%20Protocol
*/
class DovecotAuth
{
private $fp;
private $service;
private $mechanisms = [];
const VERSION_MAJOR = 1;
const VERSION_MINOR = 1;
const DEBUG = false;
public function __construct($fp, $service)
{
$this->fp = $fp;
$this->service = $service;
}
public function authenticate($username, $password)
{
$pid = getmypid();
$id = mt_rand(0, 0xffffffff);
$data = base64_encode("$username\0$username\0$password");
$this->send(['VERSION', self::VERSION_MAJOR, self::VERSION_MINOR]);
$this->send(['CPID', $pid]);
$this->recv(['DONE']);
if (!in_array('PLAIN', $this->mechanisms))
{
throw new \Exception('SASL: server does not support PLAIN mechanism');
}
$this->send(['AUTH', $id, 'PLAIN', 'service=' . $this->service]);
$this->recv(['CONT']);
$this->send(['CONT', $id, $data]);
$status = $this->recv(['OK', 'FAIL', 'CONT']);
fclose($this->fp);
return $status === 'OK';
}
private function send(array $data)
{
$str = implode("\t", $data) . "\n";
if (self::DEBUG) echo "C: $str";
fwrite($this->fp, $str);
}
private function recv(array $needles)
{
while ($line = fgets($this->fp))
{
if (self::DEBUG) echo "S: $line";
$parts = explode("\t", trim($line));
$this->handleResponseLine($parts);
if (in_array($parts[0], $needles))
{
return $parts[0];
}
}
throw new \Exception("SASL: expected server to send one of " . implode(', ', $needles));
}
private function handleResponseLine(array $parts)
{
switch ($parts[0])
{
case 'VERSION':
if ($parts[1] !== (string) self::VERSION_MAJOR)
{
throw new \Exception("SASL: version mismatch between client and server");
}
break;
case 'MECH':
$this->mechanisms[] = $parts[1];
break;
}
}
public static function createSocket($hostname, $port = -1, $timeout = 5)
{
$fp = @fsockopen($hostname, $port, $errno, $errstr, $timeout);
if (!$fp) throw new \Exception("SASL: $errstr");
stream_set_timeout($fp, $timeout);
return $fp;
}
}
?>
Auth Backend for SabreDAV that uses DovecotAuth:
<?php
class DovecotBasicAuth extends \Sabre\DAV\Auth\Backend\AbstractBasic
{
private $dovecot;
public function __construct(DovecotAuth $dovecot)
{
$this->dovecot = $dovecot;
}
protected function validateUserPass($username, $password)
{
if (!$this->authenticateUser($username, $password))
{
throw new \Sabre\DAV\Exception\NotAuthenticated('Username or password does not match');
}
return true;
}
private function authenticateUser($username, $password)
{
try
{
return $this->dovecot->authenticate($username, $password);
}
catch (\Exception $e)
{
return false;
}
}
}
?>
Usage:
$dovecot_auth_socket = DovecotAuth::createSocket('unix:///var/run/dovecot/auth-client');
$dovecot_auth = new DovecotAuth($dovecot_auth_socket, 'DAV');
$authBackend = new DAV\DovecotBasicAuth($pdo, $dovecot_auth);
I'm using this code with SabreDAV 1.8.3 and Dovecot 2.1.15. The "DovecotAuth" class only supports the "PLAIN" method because that's all I need.
Thank you very much for sharing this! Great!
Here is what works for me:
/* IMAP authentication
First argument of PHP function imap_open.
Details: http://php.net/manual/en/function.imap-open.php */
$imap_open = '{localhost:143}INBOX';
class:
namespace Sabre\DAV\Auth\Backend;
class ImapAuth extends \Sabre\DAV\Auth\Backend\AbstractBasic
{
private $imap;
public function __construct($imap)
{
$this->imap = $imap;
}
protected function validateUserPass($username, $password)
{
if (!$this->authenticateUser($username, $password))
{
throw new \Sabre\DAV\Exception\NotAuthenticated('Username or password does not match');
}
return true;
}
private function authenticateUser($username, $password)
{
try
{
if($imap = @imap_open($this->imap, $username, $password)){
imap_close($imap);
return true;
}
else{
return false;
}
}
catch (\Exception $e)
{
return false;
}
}
}
Again, previous comment is broken.
Config:
/* IMAP authentication First argument of PHP function imap_open. Details: http://php.net/manual/en/function.imap-open.php */ $imap_open = '{localhost:143}INBOX';
Init in server file:
$authBackend = new \Sabre\DAV\Auth\Backend\ImapAuth($imap_open);
Class
namespace Sabre\DAV\Auth\Backend; class ImapAuth extends \Sabre\DAV\Auth\Backend\AbstractBasic { private $imap; public function __construct($imap) { $this->imap = $imap; } protected function validateUserPass($username, $password) { if (!$this->authenticateUser($username, $password)) { throw new \Sabre\DAV\Exception\NotAuthenticated('Username or password does not match'); } return true; } private function authenticateUser($username, $password) { try { if($imap = @imap_open($this->imap, $username, $password)){ imap_close($imap); return true; } else{ return false; } } catch (\Exception $e) { return false; } } }
Steffen, Rosali,
I just came over this discussion as I'm looking for a solution to authenticate sabredav users against dovecot-sasl. As I'm no php developer, I'm unfortuantely not able to apply the code changes... Would you please give me some instructions ?
Many thanks in advance
Flo
@Flo-63
IMAP authentication is implemented in our SabreDAV plugin for Roundcube Webmail.
Hello!
I know this issue is old, however I had the same problem, and the example above is broken. As this page often comes in the firsts results of search engines, I post my solution for people who might want to connect their dovecot to sabredav ;-)
In the example of Steffenweber, there was an error concerning the mt_rand
function: 0xffffffff
was returning -1 and mt_rand(0, -1)
raises an error... and as exceptions didn't display an error message, it was not very clear.
I have juste changed 0xffffffff
by mt_getrandmax()
and raised messages when exceptions occur.
My corrected configuration (dovecot-sasl.php
file):
<?php
/**
* http://wiki.dovecot.org/Authentication%20Protocol
*/
class DovecotAuth
{
private $fp;
private $service;
private $mechanisms = [];
const VERSION_MAJOR = 1;
const VERSION_MINOR = 1;
const DEBUG = false;
public function __construct($fp, $service)
{
$this->fp = $fp;
$this->service = $service;
}
public function authenticate($username, $password)
{
$pid = getmypid();
$id = mt_rand(0, mt_getrandmax());
$data = base64_encode("$username\0$username\0$password");
$this->send(['VERSION', self::VERSION_MAJOR, self::VERSION_MINOR]);
$this->send(['CPID', $pid]);
$this->recv(['DONE']);
if (!in_array('PLAIN', $this->mechanisms))
{
throw new \Exception('SASL: server does not support PLAIN mechanism');
}
$this->send(['AUTH', $id, 'PLAIN', 'service=' . $this->service]);
$this->recv(['CONT']);
$this->send(['CONT', $id, $data]);
$status = $this->recv(['OK', 'FAIL', 'CONT']);
fclose($this->fp);
return $status === 'OK';
}
private function send(array $data)
{
$str = implode("\t", $data) . "\n";
if (self::DEBUG) echo "Send: $str";
fwrite($this->fp, $str);
}
private function recv(array $needles)
{
while ($line = fgets($this->fp))
{
if (self::DEBUG) echo "Recv: $line";
$parts = explode("\t", trim($line));
$this->handleResponseLine($parts);
if (in_array($parts[0], $needles))
{
return $parts[0];
}
}
throw new \Exception("SASL: expected server to send one of " . implode(', ', $needles));
}
private function handleResponseLine(array $parts)
{
switch ($parts[0])
{
case 'VERSION':
if ($parts[1] !== (string) self::VERSION_MAJOR)
{
throw new \Exception("SASL: version mismatch between client and server");
}
break;
case 'MECH':
$this->mechanisms[] = $parts[1];
break;
}
}
public static function createSocket($hostname, $port = -1, $timeout = 5)
{
$fp = @fsockopen($hostname, $port, $errno, $errstr, $timeout);
if (!$fp) throw new \Exception("SASL: $errstr");
stream_set_timeout($fp, $timeout);
return $fp;
}
}
//Auth Backend for SabreDAV that uses DovecotAuth
class DovecotBasicAuth extends \Sabre\DAV\Auth\Backend\AbstractBasic
{
private $dovecot;
public function __construct(DovecotAuth $dovecot)
{
$this->dovecot = $dovecot;
}
protected function validateUserPass($username, $password)
{
if (!$this->authenticateUser($username, $password))
{
throw new \Sabre\DAV\Exception\NotAuthenticated('Username or password does not match');
}
return true;
}
private function authenticateUser($username, $password)
{
try
{
return $this->dovecot->authenticate($username, $password);
}
catch (\Exception $e)
{
if (self::DEBUG) echo "authenticateUser Exception: $e";
return false;
}
}
}
?>
Do not forget to add this in the index/php
file:
require 'dovecot-sasl.php';
$dovecot_auth_socket = DovecotAuth::createSocket('unix:///var/run/dovecot/auth-client');
$dovecot_auth = new DovecotAuth($dovecot_auth_socket, 'DAV');
//$authBackend = new \Sabre\DAV\Auth\Backend\PDO($pdo);
$authBackend = new DovecotBasicAuth($dovecot_auth);
The occurence of an exception probably depends on your system architecture. I had no problems with using 0xffffffff
but mt_getrandmax()
is definetely better. Thanks!
don't ever use @
, you want those errors to show up.
I'm closing this on the assumption that there's not enough people who will end up needing this. If this assumption is wrong, please comment here. A few +1's along with your use case might convince me otherwise.
+1
+1
Ok so I changed my mind and I'm interested again. Sorry for the flip-flopping.
The big thing I need for this to be succesful, is to have some way to automatically test this. Preferably on travis-ci. Since you guys all might know something about IMAP, is anyone aware of a compact, super simple IMAP server I could install automatically so I can run functional/unit tests against it?
Might have found something. I could run 'greenmail' as a docker service inside travis:
http://www.icegreen.com/greenmail/#deploy_docker_standalone
It's gonna increase the load of the travis tests quite a bit I imagine. But open for other suggestions as well. The simpler the better.
haven't tried it yet, but there are even standalone php-built imap servers like https://github.com/TheFox/imapd
+1
For what it's worth, I've been successfully using something quite similar to what rosali posted for a few years now, against a postfix/dovecot mail server. It's simple yet effective and uses basic php functionalities - I think it should work with most imap servers as well, and there are plenty of options you can add. As per testing, sorry but no idea from my side…
@evert maybe you should also "just blog" about this topic with a recommended way to handle auth via imap instead of implementing a separate class in sabre/dav itself.
@minami-o +1. I am also happily using the ImapAuth
class.
+1
All this needs is for someone to:
It's a relatively easy one to pick up! Is there not one person among all the +1's that is willing to put in the effort? :)
Thanks Evert for the heads up, I'd be proud to contribute.Right now I definitely have no time for this, but in two weeks I should have some time to spend on this.Anyone feel free to take care of this in the meantime :)fruux/sabre-dav a écrit :All this needs is for someone to:
Grab that class, and update it for the sabre/dav 3 auth api Turn it into a pull request Add a unittest.
It's a relatively easy one to pick up! Is there not one person among all the +1's that is willing to put in the effort? :)
—You are receiving this because you were mentioned.Reply to this email directly or view it on GitHub
Envoyé via Firefox OS
+1
+1
Implemented via #891. Thanks @c0d3z3r0
I'd really love to see a method to authenticate against an IMAP server.
We recommend to use SabreDAV along with our MyRoundcube plugins (http://myroundcube.com/myroundcube-plugins/sabredav-plugin).
It would be very handy to such an authentication backend when integrating SabreDAV into a Webmail suite.