Anatomy of a Horde login - Part 2

This is the 2nd part of my adventures through the Horde authentication code. As promised, I'm covering transparent authentication and verification of existing authentication this time.


When we walked through Horde's login.php in the 1st part of this series, I mentioned the $registry->isAuthenticated() call a few times.
It is very different from the $registry->getAuth() method which returns whether a user is authenticated, too. The difference is that getAuth() simply returns the (authenticated) user name currently registered in the Horde session. isAuthenticated() does much more than that. Let's dive into the code:

/* Check for cached authentication results. */
if ($this->getAuth() &&
    (($app == 'horde') ||
     $GLOBALS['session']->exists('horde', 'auth_app/' . $app))) {
    return $this->checkExistingAuth();

Excuse the comment, it's a modest little guy. Of course this does check for cached authentication. getAuth() returns the current authenticated user (see above), and that's even sufficient if the the application that we want to verify ($app) is Horde. If this is a different application like IMP though, we require that this application is authenticated too. That's what the exists() calls does. Remember from the last article that each application requires authentication, even though it happens transparently most of the time.

One could think that we should be happy with that, now that we know the user is authenticated. But no, we further verify the authentication with checkExistingAuth().

Verifying authentication

Horde_Registry::checkExistingAuth() is really picky. Your IP address changed since you logged in? Boom:

if (!empty($GLOBALS['conf']['auth']['checkip']) &&
    ($remoteaddr = $session->get('horde', 'auth/remoteAddr')) &&
    ($remoteaddr != $_SERVER['REMOTE_ADDR'])) {
    return false;

Do you remember the setError() call from part 1? That's what we use to set a logout reason when throwing a user out of his session. Without that, Horde wouldn't know it has to log the user out in login.php, and the user wouldn't know why he's being logged out.

Your browser changes since you logged in? Boom:

if (!empty($GLOBALS['conf']['auth']['checkbrowser']) &&
    ($session->get('horde', 'auth/browser') != $GLOBALS['browser']->getAgentString())) {
    return false;

Even the base authentication driver can invalidate an existing authentication:

if ($auth->validateAuth()) {
    return true;

This can be used by SSO drivers to verify that the user is still authenticated at the central authentication system for example.

Back in isAuthenticated() the verification result is returned right away. This might be a bit confusing, because even if the current authentication is no longer valid, we might be using transparent authentication further down in the method.
That's not a problem though, because in most cases the user is thrown to login.php to get logged out if verification fails, and this is where the user can be authenticated transparently again. Since we pass the current URL to the login page when the user is loged out, he will immediately get back to the current page after his excursion to the login page and a successful renewed transparent authentication. He might not even notice.

Transparent authentication

We're still inside the isAuthenticated() method, and already covered verification of an existing authentication. Now it's time to look into transparent authentication, i.e. authentication without user interaction:

/* Try transparent authentication. */
try {
    return empty($options['notransparent'])
        ? $GLOBALS['injector']->getInstance('Horde_Core_Factory_Auth')->create($app)->transparent()
        : false;
} catch (Horde_Exception $e) {
    return false;

We can explicitly disallow transparent authentication in the method call, and a failing transparent authentication just returns false too.

We already know the singleton factory $injector->getInstance('Horde_Core_Factory_Auth')->create($app) call from the last article. To refresh our memory: it always returns a Horde_Core_Auth_Application object. If authenticating to Horde, that object has a "real" authentication driver attached.

So this bunch of code finally boils down to: transparent(). Let's look into this method:

if (!($userId = $this->getCredential('userId'))) {
    $userId = $registry->getAuth();
if (!($credentials = $this->getCredential('credentials'))) {
    $credentials = $registry->getAuthCredential();

First we try to get some existing credentials, including the user name, either from the current authentication object itself (resp. the attached authentication driver if there is one), or from the registry. Beware, the user name stored in the registry might not be the same stored in this authentication object. User names are run through the authusername hook before stored in the registry resp. the user session. (I left this out in the first version or part 1, and updated it since then)
If the user is already authenticated, for example to Horde while we are trying to authenticate to a different application, this is successful. If this is a login to Horde, the credentials are empty.

list($userId, $credentials) =
    $this->runHook($userId, $credentials, 'preauthenticate', 'transparent');

Ah, we didn't run a hook for a long time. So we run the preauthenticate hook again. "Again" because we already did this in the part 1 while processing logins through the login form. The difference is that we pass a different authentication method to this hook, "transparent". This parameter will be available inside the preauthenticate hook via the "authMethod" option, so you can run different logic depending on the authentication method. That's the only difference, you can still deny authentication from the hook, or modify the user name and credentials.

$this->setCredential('userId', $userId);
$this->setCredential('credentials', $credentials);

We store the credentials in the authentication object. Why? I'm not sure. But please note that this doesn't say anything about successful authentication. The definite source whether a user is authenticated or not is the registry, where a successful authentication is stored. See the "Storing authentication information" section in the last article.

But now let's really authenticate:

if ($this->_base) {
    $result = $this->_base->transparent();
} elseif ($this->hasCapability('transparent')) {
    $result = $registry->callAppMethod(
        array('args' => array($this), 'noperms' => true));
} else {
    /* If this application contains neither transparent nor
     * authenticate capabilities, it does not require any
     * authentication if already authenticated to Horde. */
    $result = ($registry->getAuth() && !$this->hasCapability('authenticate'));

There are three possible scenarios:

  1. This is authentication for Horde, so a base authentication driver is attached. Check this driver whether transparent authentication is possible and successful.
  2. This is an authentication for an application, and this application provides the capability to authenticate transparently. Think of IMP with the "hordeauth" setting enabled.
  3. This is an authentication for an application that doesn't provide any authentication capability and the user is already authenticated to Horde. This is the default case for the vast majority of Horde applications. And it's sufficient for the user to flag him authenticated to this application.

There's only one thing left to do. If any of those checks returned a successful authentication, we need to register the authentication. And again I refer to "Storing authentication information":

return $result && $this->_setAuth();

That's it. Horde_Registry::isAuthenticated() returns the transparent authentication result as a final resort to consider a user authenticated.


I warned you that Horde authentication is complex, I guess I didn't promise too much. But I hope these two articles help a bit to understand the logic flow during authentication, where exactly the different hooks are called, etc.

I would really like to further expand on this with a flow chart, and looking into IMP authentication. But don't hold your breath on that.