Anatomy of a Horde login
Authentication in Horde is complex. Very complex. To help you (and myself) to understand how exactly authenticating to Horde works, I tried to dissect the processes involved.
There are two different actions I'd like to look into: explicitely logging in through the login form, which happens exactly once during a session; and verifying authentication, which happens on (almost) each HTTP request. Somewhere inbetween is transparent authentication which works without using a login form, and can be triggered by verifying authentication, but still sets up the user's session like a non-transparent login.
A few notes on the (somewhat inconsistent) nomenclature:
"Horde" is most of time the Horde application, i.e. the base application that is always installed when using a complete Horde system. Sometimes, for example in the next paragraph, I use for the complete Horde installation.
"Horde_Auth" is the general purpose authentication library, available as a PEAR package from http://pear.horde.org/.
"Horde_Core" is a very special library that contains code only necessary when using Horde applications. But then it contains the most central library code.
A feature of Horde authentication to keep in mind is, that there is a separate authentication for each application. The "main" application is Horde itself, but any other application requires authentication too. In most cases this happens transparently, i.e. Horde's authentication is re-used anywhere else. But there are applications that may require separate authentication, e.g. logging in to a mail server in IMP, or to an FTP server in Gollem.
Let's start with the non-transparent login via the Horde login form.
Login form
Any login form in Horde is provided by horde/login.php. The page itself (of course) doesn't require authentication, so we initialize the Horde registry without requesting authentication:
Horde_Registry::appInit( 'horde', array('authentication' => 'none', 'nologintasks' => true));
Next we check if the user is already authenticated:
$is_auth = $registry->isAuthenticated();
This serves two purposes: it checks if the user accesses the login page even though he's already authenticated, for example to log out of Horde. Logging out happens in login.php too. And it allows for transparent authentication, that we will talk about later.
If the user isn't authenticated yet, we generate a secret key that we are going to use later to encrypt confidential credentials, e.g the login password. This isn't necessary if the user is already authenticated, because this key is kept during the user's session in a browser cookie (if the user disabled cookies, we fall back to using the session id as the key instead, which is of course less secure):
$injector->getInstance('Horde_Secret')->setKey('auth');
This key isn't kept in a variable, because we can later retrieve it anywhere in Horde with getKey('auth').
Authentication factory
Now it's time to create an authentication object:
$auth = $injector ->getInstance('Horde_Core_Factory_Auth') ->create(($is_auth && $vars->app) ? $vars->app : null);
This already looks more complicated, but it's still nothing compared to what happens inside this call. What this call does is to ask an internal factory to create and return a Horde_Auth object. If an application parameter is passed to login.php ($vars->app) and if the user is already authenticated (to Horde, not to the application, see $is_auth), then we are requesting a Horde_Auth object for that application, otherwise one for Horde.
Now let's see what happens inside. This factory is supposed to return a Horde_Auth object, which means an actual authentication driver class extending the abstract Horde_Auth_Base class. This is the class/object authenticating against a "real" authentication backend, managing users, etc. I probably shouldn't say Horde_Auth object, because the class of the same name is just a static class providing some constants and helper methods. When I say Horde_Auth object, I mean an instance of a class extending Horde_Auth_Base.
This factory method implements a singleton pattern and always returns a Horde_Core_Auth_Application object: a special authentication driver implementing the regular, simple Horde_Auth API, but providing a lot of other stuff that we actually need when using Horde_Auth within the application framework. This stuff is very special to Horde applications, that's why it's part of the Horde_Core package, not the Horde_Auth package, that can be used by any 3rd party developers in need for some authentication library and not interested in developing Horde applications.
If the application passed to the factory is Horde itself, this Horde_Core_Auth_Application object will get a "real" Horde_Auth object attached. Remember: Horde_Core_Auth_Application will only do Horde-specific authentication stuff. When it comes to actual authentication, it is going to be delegated to this attached Horde_Auth driver object, which could for example implement SQL, PAM, or IMAP authentication.
It's not important to know for this article, but this factory also pulls in any dependencies and configuration from a Horde installation that such a Horde_Auth driver needs, in some case even extending the driver class.
Logging out
Back to login.php, we have our (decorated and configured) authentication object in $auth. We test if there is a reason to log the user out:
if (!($logout_reason = $auth->getError())) { $logout_reason = $vars->logout_reason; }
The first statement checks if the authentication object provides some error. We just created the object, so how can it already return an error? Note that the authentication factory implements a singleton, i.e. we only have a single authentication object anywhere. This object could have been created earlier. Remember the isAuthenticated() call from above? This is such a place, that we ignored so far.
A logout reason could also be passed in an URL parameter.
Logout reasons can be the user clicking a logout link, a session timing out, the user's IP address or browser agent string changing, etc.
The next part of login.php is split in two. If we have a logout reason, we are finally logging the user out, destroying his session, cleaning up behind him. But this is not what we are interested in.
Logging in
We are interested in the part that checks if a login form has been submitted:
if (Horde_Util::getPost('login_post') || Horde_Util::getPost('login_button')) { /* Get the login params from the login screen. */ $auth_params = array( 'password' => Horde_Util::getPost('horde_pass'), 'mode' => Horde_Util::getPost('horde_select_view') ); try { $result = $auth->getLoginParams(); foreach (array_keys($result['params']) as $val) { $auth_params[$val] = Horde_Util::getPost($val); } } catch (Horde_Exception $e) {}
What we do is to collect all login data we need. Horde_Util::getPost() is returning POST data from the form. Which form fields we might have to check is returned by $auth->getLoginParams(). And this is the first of Horde_Core_Auth_Application extensions to the authentication API. The same method is used to dynamically build the login form with all fields that may be required. If we are authenticating to Horde, this list might be provided by the base authentication driver (though this is not used by any driver yet). If we are authenticating to a Horde application, this application might provide such a list. It's used by IMP to include the server drop down list on the login screen for example.
But now it happens. Really. We authenticate:
$auth->authenticate(Horde_Util::getPost('horde_user'), $auth_params)
If authentication succeeds, we check if the user's password had expired and needs to be updated ($registry->passwordChangeRequested()), or load the main page (require HORDE_BASE . '/index.php';).
If authentication fails we retrieve the reason ($logout_reason = $auth->getError();) to notify the user before throwing him back to the login screen.
Inside Horde_Core_Auth_Application
But what happens inside $auth->authenticate()? The magic.
First, we run the preauthenticate hook:
try { list($userId, $credentials) = $this->runHook(trim($userId), $credentials, 'preauthenticate', 'authenticate'); } catch (Horde_Auth_Exception $e) { return false; }
This hook has two purposes. Administrators can run additional checks before even attempting to authenticate against the authentication backend. For example temporarily disabling logins during maintenance or high load, or for certain users, or blacklisted IP addresses.
But the hook can also change the credentials before they are passed to the authentication driver. This would change the user name from what the user entered in the login form, to what is used at any later point - inside Horde, or at the authentication backend. It could be used to attach domain names in a virtual host setup etc.
Next we delegate the authentication further. If this is Horde authentication, to the base driver, if this is application authentication to the parent class:
if ($this->_base) { if (!$this->_base->authenticate($userId, $credentials, $login)) { return false; } } elseif (!parent::authenticate($userId, $credentials, $login)) { return false; }
Fortunately there isn't any authentication driver overwriting the base class' authenticate() method, so the implementation we have to look at is the same in both cases:
$this->_credentials['userId'] = $userId; $this->_authenticate($userId, $credentials); $this->setCredential('userId', $this->_credentials['userId']); $this->setCredential('credentials', $credentials);
This looks a bit strange. Why are we setting the userId credential once directly and once using setCredential()? The reason is that we want to allow a driver's _authenticate() method to rewrite userId. But that's really an odd edge case that is only used with Kolab at the moment. We can ignore that.
More important is the _authenticate() call. This is the bare authentication method implemented in each authentication driver. Note that $this is the backend driver implementing the Horde_Auth API if we authenticate to Horde, but a Horde_Core_Auth_Application object if we authenticate to an application.
A backend driver checks the passed user name and credentials against the authentication backend, e.g. by trying to login in to an IMAP or LDAP server with the user name and password, or verifying those against a user list in a database table.
A Horde_Core_Auth_Application object calls the authAuthenticate API method of the application that we are authenticating for. It might be worth a separate article to look specifically at that API call, at least in the case of IMP, because there happens a lot in IMP, too, when authenticating.
If authentication failed, an exception is thrown that we immediately catch in Horde_Base::authenticate(), to propagate the object error property with the exception message. This is what is later used as $logout_reason in login.php.
But if authentication succeeded, we store the successful credentials with setCredential().
We're jumping back to Horde_Core_Auth_Application::authenticate() where only one statement is left:
return $this->_setAuth();
Storing authentication information
But you already knew it, this method does a lot of magic again. To avoid this method being called more than once, we check if the user already is authenticated. Believe it or not, from Horde's point of view, he still isn't.
if ($registry->isAuthenticated(array('app' => $this->_app, 'notransparent' => true))) { return true; }
Let's make sure we have a fresh session. This is to protect against session fixation:
$registry->getCleanSession();
We also read the user name and credentials we stored earlier, and then call the postauthenticate hook:
list(,$credentials) = $this->runHook($userId, $credentials, 'postauthenticate');
Again, this hook serves two purposes, just like the preauthenticate hook. It's the last chance for an administrator to deny authentication, for example if denial depends on information that is only available after authenticating against the backend.
And this hook can modify the credentials too. But note the subtle difference to how we call the preauthenticate hook. The postauthenticate hook does not allow to modify the user name anymore.
Thanks for keeping with me so far. If you did, you will reach the climax with me right now:
$registry->setAuth($userId, $credentials, array( 'app' => $this->_app, 'change' => $this->getCredential('change'), 'language' => $language ));
Yes, it's what it looks like. We are finally telling Horde that this user is authenticated. At least for the application that we pass to the method. What it does is storing the credentials for the application that we authenticated to (using the secret key from login.php by the way). And if this application happens to be Horde, we store all kind of other information like user name and a timestamp, load the user preferences, call more hooks etc.
[Update: It's worth noting that isAuth() is running the user name through the authusername hook before storing it for future reference. It does keep a copy of the original user name too, but $registry->getAuth() is always returning the modified version. This is important to know, because that modified user name is used when authenticating transparently to a Horde application.
This hook is usually used if you need to avoid ambiguous user names in Horde, e.g. by lowercasing all users if the authentication backend is case insensitive, or by attaching a realm if user names are not unique, i.e. the same Horde system is used for separate authentication backends.]
The only thing noteable that happens after that is running a callback method in the application's API that we authenticated to:
$registry->callAppMethod($this->_app, 'authAuthenticateCallback', array('noperms' => true));
This again would be interesting for a separate article that goes into details with authenticating to IMP.
But that's it, we read the login form fields submitted by the user, jumped through a few loops and hooks, authenticated against the authentication backend, and stored the authentication information in Horde's session.
Outlook
The next article will cover transparent authentication, i.e. what happens in the mysterious isAuthenticated() call, and verifying an existing authentication.
I would really like to add a flow chart to help understanding the logic flow. Does anybody know a good OSS software for that, that is not just a beefed up chart drawing tool? I'm rather looking for something that allows me to enter the logical dependencies and branches of nodes, and then draws (at least an initial version) of the chart on its own. Add a comment or drop me a line if you know one.