sabre-io / dav

sabre/dav is a CalDAV, CardDAV and WebDAV framework for PHP
http://sabre.io
BSD 3-Clause "New" or "Revised" License
1.54k stars 347 forks source link

Authentication against IMAP server #322

Closed rosali closed 8 years ago

rosali commented 11 years ago

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.

steffenweber commented 11 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.

rosali commented 11 years ago

Thank you very much for sharing this! Great!

rosali commented 11 years ago

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;
        }
    }
}
rosali commented 11 years ago

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;
        }
    }
}
Flo-63 commented 10 years ago

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

myroundcube commented 10 years ago

@Flo-63

IMAP authentication is implemented in our SabreDAV plugin for Roundcube Webmail.

Biniou180 commented 9 years ago

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);
steffenweber commented 9 years ago

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!

evert commented 9 years ago

don't ever use @, you want those errors to show up.

evert commented 8 years ago

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.

bobsoppe commented 8 years ago

+1

gahr commented 8 years ago

+1

evert commented 8 years ago

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?

evert commented 8 years ago

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.

staabm commented 8 years ago

haven't tried it yet, but there are even standalone php-built imap servers like https://github.com/TheFox/imapd

thekoma commented 8 years ago

+1

minami-o commented 8 years ago

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…

staabm commented 8 years ago

@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.

gahr commented 8 years ago

@minami-o +1. I am also happily using the ImapAuth class.

thekoma commented 8 years ago

+1

evert commented 8 years ago

All this needs is for someone to:

  1. Grab that class, and update it for the sabre/dav 3 auth api
  2. Turn it into a pull request
  3. 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? :)

minami-o commented 8 years ago

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

bakerjalexander commented 8 years ago

+1

Hitman86R commented 8 years ago

+1

c0d3z3r0 commented 8 years ago

891

evert commented 8 years ago

Implemented via #891. Thanks @c0d3z3r0