PageRenderTime 71ms CodeModel.GetById 3ms app.highlight 52ms RepoModel.GetById 1ms app.codeStats 1ms

/horde-3.3.13/lib/Horde/Auth.php

#
PHP | 1375 lines | 721 code | 148 blank | 506 comment | 136 complexity | b4942f1ad38666718c5d99fceea8bc4e MD5 | raw file
   1<?php
   2/**
   3 * The parameter name for the logout reason.
   4 */
   5define('AUTH_REASON_PARAM', 'logout_reason');
   6
   7/**
   8 * The parameter name for the logout message used with type
   9 * AUTH_REASON_MESSAGE.
  10 */
  11define('AUTH_REASON_MSG_PARAM', 'logout_msg');
  12
  13/**
  14 * The 'badlogin' reason.
  15 *
  16 * The following 'reasons' for the logout screen are recognized:
  17 * <pre>
  18 *   'badlogin'   --  Bad username and/or password
  19 *   'browser'    --  A browser change was detected
  20 *   'failed'     --  Login failed
  21 *   'expired'    --  Password has expired
  22 *   'logout'     --  Logout due to user request
  23 *   'message'    --  Logout with custom message in AUTH_REASON_MSG_PARAM
  24 *   'session'    --  Logout due to session expiration
  25 *   'sessionip'  --  Logout due to change of IP address during session
  26 * </pre>
  27 */
  28define('AUTH_REASON_BADLOGIN', 'badlogin');
  29
  30/**
  31 * The 'browser' reason.
  32 */
  33define('AUTH_REASON_BROWSER', 'browser');
  34
  35/**
  36 * The 'failed' reason.
  37 */
  38define('AUTH_REASON_FAILED', 'failed');
  39
  40/**
  41 * The 'expired' reason.
  42 */
  43define('AUTH_REASON_EXPIRED', 'expired');
  44
  45/**
  46 * The 'logout' reason.
  47 */
  48define('AUTH_REASON_LOGOUT', 'logout');
  49
  50/**
  51 * The 'message' reason.
  52 */
  53define('AUTH_REASON_MESSAGE', 'message');
  54
  55/**
  56 * The 'session' reason.
  57 */
  58define('AUTH_REASON_SESSION', 'session');
  59
  60/**
  61 * The 'sessionip' reason.
  62 */
  63define('AUTH_REASON_SESSIONIP', 'sessionip');
  64
  65/**
  66 * The Auth:: class provides a common abstracted interface into the various
  67 * backends for the Horde authentication system.
  68 *
  69 * $Horde: framework/Auth/Auth.php,v 1.142.10.37 2009/10/26 11:58:58 jan Exp $
  70 *
  71 * Copyright 1999-2009 The Horde Project (http://www.horde.org/)
  72 *
  73 * See the enclosed file COPYING for license information (LGPL). If you
  74 * did not receive this file, see http://opensource.org/licenses/lgpl-license.php.
  75 *
  76 * @author  Chuck Hagenbuch <chuck@horde.org>
  77 * @since   Horde 1.3
  78 * @package Horde_Auth
  79 */
  80class Auth {
  81
  82    /**
  83     * An array of capabilities, so that the driver can report which
  84     * operations it supports and which it doesn't.
  85     *
  86     * @var array
  87     */
  88    var $capabilities = array('add'           => false,
  89                              'update'        => false,
  90                              'resetpassword' => false,
  91                              'remove'        => false,
  92                              'list'          => false,
  93                              'groups'        => false,
  94                              'admins'        => false,
  95                              'transparent'   => false);
  96
  97    /**
  98     * Hash containing parameters.
  99     *
 100     * @var array
 101     */
 102    var $_params = array();
 103
 104    /**
 105     * The credentials currently being authenticated.
 106     *
 107     * @access protected
 108     *
 109     * @var array
 110     */
 111    var $_authCredentials = array();
 112
 113    /**
 114     * Returns the name of the concrete Auth implementation.
 115     *
 116     * @return string  The Auth implementation name.
 117     */
 118    function getDriver()
 119    {
 120        return str_replace('auth_', '', strtolower(get_class($this)));
 121    }
 122
 123    /**
 124     * Finds out if a set of login credentials are valid, and if requested,
 125     * mark the user as logged in in the current session.
 126     *
 127     * @param string $userId      The userId to check.
 128     * @param array $credentials  The credentials to check.
 129     * @param boolean $login      Whether to log the user in. If false, we'll
 130     *                            only test the credentials and won't modify
 131     *                            the current session. Defaults to true.
 132     * @param string $realm       The authentication realm to check.
 133     *
 134     * @return boolean  Whether or not the credentials are valid.
 135     */
 136    function authenticate($userId, $credentials, $login = true, $realm = null)
 137    {
 138        $auth = false;
 139        $userId = trim($userId);
 140
 141        if (!empty($GLOBALS['conf']['hooks']['preauthenticate'])) {
 142            if (!Horde::callHook('_horde_hook_preauthenticate', array($userId, $credentials, $realm), 'horde', false)) {
 143                if ($this->_getAuthError() != AUTH_REASON_MESSAGE) {
 144                    $this->_setAuthError(AUTH_REASON_FAILED);
 145                }
 146                return false;
 147            }
 148        }
 149
 150        /* Store the credentials being checked so that subclasses can modify
 151         * them if necessary (like transparent auth does). */
 152        $this->_authCredentials = array(
 153            'userId' => $userId,
 154            'credentials' => $credentials,
 155            'realm' => $realm,
 156            'changeRequested' => false
 157        );
 158
 159        if ($authenticated = $this->_authenticate($userId, $credentials)) {
 160            if (is_a($authenticated, 'PEAR_Error')) {
 161                return false;
 162            }
 163
 164            if ($login) {
 165                $auth = $this->setAuth(
 166                    $this->_authCredentials['userId'],
 167                    $this->_authCredentials['credentials'],
 168                    $this->_authCredentials['realm'],
 169                    $this->_authCredentials['changeRequested']);
 170            } else {
 171                if (!$this->_checkSessionIP()) {
 172                    $this->_setAuthError(AUTH_REASON_SESSIONIP);
 173                    return false;
 174                } elseif (!$this->_checkBrowserString()) {
 175                    $this->_setAuthError(AUTH_REASON_BROWSER);
 176                    return false;
 177                }
 178                $auth = true;
 179            }
 180        }
 181
 182        return $auth;
 183    }
 184
 185    /**
 186     * Formats a password using the current encryption.
 187     *
 188     * @param string $plaintext      The plaintext password to encrypt.
 189     * @param string $salt           The salt to use to encrypt the password.
 190     *                               If not present, a new salt will be
 191     *                               generated.
 192     * @param string $encryption     The kind of pasword encryption to use.
 193     *                               Defaults to md5-hex.
 194     * @param boolean $show_encrypt  Some password systems prepend the kind of
 195     *                               encryption to the crypted password ({SHA},
 196     *                               etc). Defaults to false.
 197     *
 198     * @return string  The encrypted password.
 199     */
 200    function getCryptedPassword($plaintext, $salt = '',
 201                                $encryption = 'md5-hex', $show_encrypt = false)
 202    {
 203        /* Get the salt to use. */
 204        $salt = Auth::getSalt($encryption, $salt, $plaintext);
 205
 206        /* Encrypt the password. */
 207        switch ($encryption) {
 208        case 'plain':
 209            return $plaintext;
 210
 211        case 'msad':
 212            return String::convertCharset('"' . $plaintext . '"', 'ISO-8859-1', 'UTF-16LE');
 213
 214        case 'sha':
 215            $encrypted = base64_encode(pack('H*', sha1($plaintext)));
 216            return ($show_encrypt) ? '{SHA}' . $encrypted : $encrypted;
 217
 218        case 'crypt':
 219        case 'crypt-des':
 220        case 'crypt-md5':
 221        case 'crypt-blowfish':
 222            return ($show_encrypt ? '{crypt}' : '') . crypt($plaintext, $salt);
 223
 224        case 'md5-base64':
 225            $encrypted = base64_encode(pack('H*', md5($plaintext)));
 226            return ($show_encrypt) ? '{MD5}' . $encrypted : $encrypted;
 227
 228        case 'ssha':
 229            $encrypted = base64_encode(pack('H*', sha1($plaintext . $salt)) . $salt);
 230            return ($show_encrypt) ? '{SSHA}' . $encrypted : $encrypted;
 231
 232        case 'smd5':
 233            $encrypted = base64_encode(pack('H*', md5($plaintext . $salt)) . $salt);
 234            return ($show_encrypt) ? '{SMD5}' . $encrypted : $encrypted;
 235
 236        case 'aprmd5':
 237            $length = strlen($plaintext);
 238            $context = $plaintext . '$apr1$' . $salt;
 239            $binary = pack('H*', md5($plaintext . $salt . $plaintext));
 240
 241            for ($i = $length; $i > 0; $i -= 16) {
 242                $context .= substr($binary, 0, ($i > 16 ? 16 : $i));
 243            }
 244            for ($i = $length; $i > 0; $i >>= 1) {
 245                $context .= ($i & 1) ? chr(0) : $plaintext[0];
 246            }
 247
 248            $binary = pack('H*', md5($context));
 249
 250            for ($i = 0; $i < 1000; $i++) {
 251                $new = ($i & 1) ? $plaintext : substr($binary, 0, 16);
 252                if ($i % 3) {
 253                    $new .= $salt;
 254                }
 255                if ($i % 7) {
 256                    $new .= $plaintext;
 257                }
 258                $new .= ($i & 1) ? substr($binary, 0, 16) : $plaintext;
 259                $binary = pack('H*', md5($new));
 260            }
 261
 262            $p = array();
 263            for ($i = 0; $i < 5; $i++) {
 264                $k = $i + 6;
 265                $j = $i + 12;
 266                if ($j == 16) {
 267                    $j = 5;
 268                }
 269                $p[] = Auth::_toAPRMD5((ord($binary[$i]) << 16) |
 270                                       (ord($binary[$k]) << 8) |
 271                                       (ord($binary[$j])),
 272                                       5);
 273            }
 274
 275            return '$apr1$' . $salt . '$' . implode('', $p) . Auth::_toAPRMD5(ord($binary[11]), 3);
 276
 277        case 'md5-hex':
 278        default:
 279            return ($show_encrypt) ? '{MD5}' . md5($plaintext) : md5($plaintext);
 280        }
 281    }
 282
 283    /**
 284     * Returns a salt for the appropriate kind of password encryption.
 285     * Optionally takes a seed and a plaintext password, to extract the seed
 286     * of an existing password, or for encryption types that use the plaintext
 287     * in the generation of the salt.
 288     *
 289     * @param string $encryption  The kind of pasword encryption to use.
 290     *                            Defaults to md5-hex.
 291     * @param string $seed        The seed to get the salt from (probably a
 292     *                            previously generated password). Defaults to
 293     *                            generating a new seed.
 294     * @param string $plaintext   The plaintext password that we're generating
 295     *                            a salt for. Defaults to none.
 296     *
 297     * @return string  The generated or extracted salt.
 298     */
 299    function getSalt($encryption = 'md5-hex', $seed = '', $plaintext = '')
 300    {
 301        switch ($encryption) {
 302        case 'crypt':
 303        case 'crypt-des':
 304            if ($seed) {
 305                return substr(preg_replace('|^{crypt}|i', '', $seed), 0, 2);
 306            } else {
 307                return substr(md5(mt_rand()), 0, 2);
 308            }
 309
 310        case 'crypt-md5':
 311            if ($seed) {
 312                return substr(preg_replace('|^{crypt}|i', '', $seed), 0, 12);
 313            } else {
 314                return '$1$' . substr(md5(mt_rand()), 0, 8) . '$';
 315            }
 316
 317        case 'crypt-blowfish':
 318            if ($seed) {
 319                return substr(preg_replace('|^{crypt}|i', '', $seed), 0, 16);
 320            } else {
 321                return '$2$' . substr(md5(mt_rand()), 0, 12) . '$';
 322            }
 323
 324        case 'ssha':
 325            if ($seed) {
 326                return substr(base64_decode(preg_replace('|^{SSHA}|i', '', $seed)), 20);
 327            } else {
 328                $salt = substr(pack('h*', md5(mt_rand())), 0, 8);
 329                return substr(pack('H*', sha1($salt . $plaintext)), 0, 4);
 330            }
 331
 332        case 'smd5':
 333            if ($seed) {
 334                return substr(base64_decode(preg_replace('|^{SMD5}|i', '', $seed)), 16);
 335            } else {
 336                $salt = substr(pack('h*', md5(mt_rand())), 0, 8);
 337                return substr(pack('H*', md5($salt . $plaintext)), 0, 4);
 338            }
 339
 340        case 'aprmd5':
 341            /* 64 characters that are valid for APRMD5 passwords. */
 342            $APRMD5 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
 343
 344            if ($seed) {
 345                return substr(preg_replace('/^\$apr1\$(.{8}).*/', '\\1', $seed), 0, 8);
 346            } else {
 347                $salt = '';
 348                for ($i = 0; $i < 8; $i++) {
 349                    $salt .= $APRMD5[mt_rand(0, 63)];
 350                }
 351                return $salt;
 352            }
 353
 354        default:
 355            return '';
 356        }
 357    }
 358
 359    /**
 360     * Generates a random, hopefully pronounceable, password. This can be used
 361     * when resetting automatically a user's password.
 362     *
 363     * @return string A random password
 364     */
 365    function genRandomPassword()
 366    {
 367        $vowels    = 'aeiouy';
 368        $constants = 'bcdfghjklmnpqrstvwxz';
 369        $numbers   = '0123456789';
 370
 371        /* Alternate constant and vowel random chars with two random numbers
 372         * at the end. This should produce a fairly pronounceable password. */
 373        $chars[0] = substr($constants, mt_rand(0, strlen($constants) - 1), 1);
 374        $chars[1] = substr($vowels, mt_rand(0, strlen($vowels) - 1), 1);
 375        $chars[2] = substr($constants, mt_rand(0, strlen($constants) - 1), 1);
 376        $chars[3] = substr($vowels, mt_rand(0, strlen($vowels) - 1), 1);
 377        $chars[4] = substr($constants, mt_rand(0, strlen($constants) - 1), 1);
 378        $chars[5] = substr($numbers, mt_rand(0, strlen($numbers) - 1), 1);
 379        $chars[6] = substr($numbers, mt_rand(0, strlen($numbers) - 1), 1);
 380
 381        return implode('', $chars);
 382    }
 383
 384    /**
 385     * Adds a set of authentication credentials.
 386     *
 387     * @abstract
 388     *
 389     * @param string $userId      The userId to add.
 390     * @param array $credentials  The credentials to use.
 391     *
 392     * @return mixed  True on success or a PEAR_Error object on failure.
 393     */
 394    function addUser($userId, $credentials)
 395    {
 396        return PEAR::raiseError('unsupported');
 397    }
 398
 399    /**
 400     * Updates a set of authentication credentials.
 401     *
 402     * @abstract
 403     *
 404     * @param string $oldID        The old userId.
 405     * @param string $newID        The new userId.
 406     * @param array $credentials   The new credentials
 407     *
 408     * @return mixed  True on success or a PEAR_Error object on failure.
 409     */
 410    function updateUser($oldID, $newID, $credentials)
 411    {
 412        return PEAR::raiseError('unsupported');
 413    }
 414
 415    /**
 416     * Deletes a set of authentication credentials.
 417     *
 418     * @abstract
 419     *
 420     * @param string $userId  The userId to delete.
 421     *
 422     * @return mixed  True on success or a PEAR_Error object on failure.
 423     */
 424    function removeUser($userId)
 425    {
 426        return PEAR::raiseError('unsupported');
 427    }
 428
 429    /**
 430     * Calls all applications' removeUser API methods.
 431     *
 432     * @param string $userId  The userId to delete.
 433     *
 434     * @return mixed  True on success or a PEAR_Error object on failure.
 435     */
 436    function removeUserData($userId)
 437    {
 438        global $registry;
 439
 440        $errApps = array();
 441
 442        foreach ($registry->listApps(array('notoolbar', 'hidden', 'active', 'admin')) as $app) {
 443            if ($registry->hasMethod('removeUserData', $app)) {
 444                if (is_a($result = $registry->callByPackage($app, 'removeUserData', array($userId)), 'PEAR_Error')) {
 445                    Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
 446                    $errApps[] = $app;
 447                }
 448            }
 449        }
 450
 451        if (count($errApps)) {
 452            $err = implode(', ', $errApps);
 453            return PEAR::raiseError(sprintf(_("The following applications encountered errors removing user data: %s"), $err));
 454        } else {
 455            return true;
 456        }
 457    }
 458
 459    /**
 460     * Lists all users in the system.
 461     *
 462     * @abstract
 463     *
 464     * @return mixed  The array of userIds, or a PEAR_Error object on failure.
 465     */
 466    function listUsers()
 467    {
 468        return PEAR::raiseError('unsupported');
 469    }
 470
 471    /**
 472     * Checks if $userId exists in the system.
 473     *
 474     * @abstract
 475     *
 476     * @param string $userId User ID for which to check
 477     *
 478     * @return boolean  Whether or not $userId already exists.
 479     */
 480    function exists($userId)
 481    {
 482        $users = $this->listUsers();
 483        if (is_a($users, 'PEAR_Error')) {
 484            return $users;
 485        }
 486        return in_array($userId, $users);
 487    }
 488
 489    /**
 490     * Automatic authentication.
 491     *
 492     * @abstract
 493     *
 494     * @return boolean  Whether or not the user is authenticated automatically.
 495     */
 496    function transparent()
 497    {
 498        return false;
 499    }
 500
 501    /**
 502     * Checks if there is a session with valid auth information. for the
 503     * specified user. If there isn't, but the configured Auth driver supports
 504     * transparent authentication, then we try that.
 505     *
 506     * @param string $realm  The authentication realm to check.
 507     *
 508     * @return boolean  Whether or not the user is authenticated.
 509     */
 510    function isAuthenticated($realm = null)
 511    {
 512        if (isset($_SESSION['__auth'])) {
 513            if (!empty($_SESSION['__auth']['authenticated']) &&
 514                !empty($_SESSION['__auth']['userId']) &&
 515                ($_SESSION['__auth']['realm'] == $realm)) {
 516                if (!Auth::_checkSessionIP()) {
 517                    Auth::_setAuthError(AUTH_REASON_SESSIONIP);
 518                    return false;
 519                } elseif (!Auth::_checkBrowserString()) {
 520                    Auth::_setAuthError(AUTH_REASON_BROWSER);
 521                    return false;
 522                } else {
 523                    return true;
 524                }
 525            }
 526        }
 527
 528        // Try transparent authentication now.
 529        $auth = &Auth::singleton($GLOBALS['conf']['auth']['driver']);
 530        if ($auth->hasCapability('transparent') && $auth->transparent()) {
 531            return Auth::isAuthenticated($realm);
 532        }
 533
 534        return false;
 535    }
 536
 537    /**
 538     * Returns the currently logged in user, if there is one.
 539     *
 540     * @return mixed  The userId of the current user, or false if no user is
 541     *                logged in.
 542     */
 543    function getAuth()
 544    {
 545        if (isset($_SESSION['__auth'])) {
 546            if (!empty($_SESSION['__auth']['authenticated']) &&
 547                !empty($_SESSION['__auth']['userId'])) {
 548                return $_SESSION['__auth']['userId'];
 549            }
 550        }
 551
 552        return false;
 553    }
 554
 555    /**
 556     * Return whether the authentication backend requested a password change.
 557     *
 558     * @return boolean Whether the backend requested a password change.
 559     */
 560    function isPasswordChangeRequested()
 561    {
 562        if (isset($_SESSION['__auth']) &&
 563            !empty($_SESSION['__auth']['authenticated']) &&
 564            !empty($_SESSION['__auth']['changeRequested'])) {
 565            return true;
 566        }
 567
 568        return false;
 569    }
 570
 571    /**
 572     * Returns the curently logged-in user without any domain information
 573     * (e.g., bob@example.com would be returned as 'bob').
 574     *
 575     * @return mixed  The user ID of the current user, or false if no user
 576     *                is logged in.
 577     */
 578    function getBareAuth()
 579    {
 580        $user = Auth::getAuth();
 581        if ($user) {
 582            $pos = strpos($user, '@');
 583            if ($pos !== false) {
 584                $user = substr($user, 0, $pos);
 585            }
 586        }
 587
 588        return $user;
 589    }
 590
 591    /**
 592     * Returns the domain of currently logged-in user (e.g., bob@example.com
 593     * would be returned as 'example.com').
 594     *
 595     * @since Horde 3.0.6
 596     *
 597     * @return mixed  The domain suffix of the current user, or false.
 598     */
 599    function getAuthDomain()
 600    {
 601        if ($user = Auth::getAuth()) {
 602            $pos = strpos($user, '@');
 603            if ($pos !== false) {
 604                return substr($user, $pos + 1);
 605            }
 606        }
 607
 608        return false;
 609    }
 610
 611    /**
 612     * Returns the requested credential for the currently logged in user, if
 613     * present.
 614     *
 615     * @param string $credential  The credential to retrieve.
 616     *
 617     * @return mixed  The requested credential, or false if no user is
 618     *                logged in.
 619     */
 620    function getCredential($credential)
 621    {
 622        if (!empty($_SESSION['__auth']) &&
 623            !empty($_SESSION['__auth']['authenticated'])) {
 624            require_once 'Horde/Secret.php';
 625            $credentials = Secret::read(Secret::getKey('auth'), $_SESSION['__auth']['credentials']);
 626            $credentials = @unserialize($credentials);
 627        } else {
 628            return false;
 629        }
 630
 631        if (is_array($credentials) &&
 632            isset($credentials[$credential])) {
 633            return $credentials[$credential];
 634        } else {
 635            return false;
 636        }
 637    }
 638
 639    /**
 640     * Sets the requested credential for the currently logged in user.
 641     *
 642     * @param string $credential  The credential to set.
 643     * @param string $value       The value to set the credential to.
 644     */
 645    function setCredential($credential, $value)
 646    {
 647        if (!empty($_SESSION['__auth']) &&
 648            !empty($_SESSION['__auth']['authenticated'])) {
 649            require_once 'Horde/Secret.php';
 650            $credentials = @unserialize(Secret::read(Secret::getKey('auth'), $_SESSION['__auth']['credentials']));
 651            if (is_array($credentials)) {
 652                $credentials[$credential] = $value;
 653            } else {
 654                $credentials = array($credential => $value);
 655            }
 656            $_SESSION['__auth']['credentials'] = Secret::write(Secret::getKey('auth'), serialize($credentials));
 657        }
 658    }
 659
 660    /**
 661     * Sets a variable in the session saying that authorization has succeeded,
 662     * note which userId was authorized, and note when the login took place.
 663     *
 664     * If a user name hook was defined in the configuration, it gets applied
 665     * to the userId at this point.
 666     *
 667     * @param string $userId            The userId who has been authorized.
 668     * @param array $credentials        The credentials of the user.
 669     * @param string $realm             The authentication realm to use.
 670     * @param boolean $changeRequested  Whether to request that the user change
 671     *                                  their password.
 672     */
 673    function setAuth($userId, $credentials, $realm = null, $changeRequested = false)
 674    {
 675        $userId = trim($userId);
 676        $userId = Auth::addHook($userId);
 677
 678        if (!empty($GLOBALS['conf']['hooks']['postauthenticate'])) {
 679            if (!Horde::callHook('_horde_hook_postauthenticate', array($userId, $credentials, $realm), 'horde', false)) {
 680                if ($this->_getAuthError() != AUTH_REASON_MESSAGE) {
 681                    $this->_setAuthError(AUTH_REASON_FAILED);
 682                }
 683                return false;
 684            }
 685        }
 686
 687        /* If we're already set with this userId, don't continue. */
 688        if (isset($_SESSION['__auth']['userId']) &&
 689            $_SESSION['__auth']['userId'] == $userId) {
 690            return true;
 691        }
 692
 693        /* Clear any existing info. */
 694        $this->clearAuth($realm);
 695
 696        require_once 'Horde/Secret.php';
 697        $credentials = Secret::write(Secret::getKey('auth'), serialize($credentials));
 698
 699        if (!empty($realm)) {
 700            $userId .= '@' . $realm;
 701        }
 702
 703        $_SESSION['__auth'] = array(
 704            'authenticated' => true,
 705            'userId' => $userId,
 706            'credentials' => $credentials,
 707            'realm' => $realm,
 708            'timestamp' => time(),
 709            'remote_addr' => isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null,
 710            'browser' => $GLOBALS['browser']->getAgentString(),
 711            'changeRequested' => $changeRequested
 712        );
 713
 714        /* Reload preferences for the new user. */
 715        $GLOBALS['registry']->loadPrefs();
 716        NLS::setLang($GLOBALS['prefs']->getValue('language'));
 717
 718        /* Fetch the user's last login time. */
 719        $old_login = @unserialize($GLOBALS['prefs']->getValue('last_login'));
 720
 721        /* Display it, if we have a notification object and the
 722         * show_last_login preference is active. */
 723        if (isset($GLOBALS['notification']) && $GLOBALS['prefs']->getValue('show_last_login')) {
 724            if (empty($old_login['time'])) {
 725                $GLOBALS['notification']->push(_("Last login: Never"), 'horde.message');
 726            } else {
 727                if (empty($old_login['host'])) {
 728                    $GLOBALS['notification']->push(sprintf(_("Last login: %s"), strftime('%c', $old_login['time'])), 'horde.message');
 729                } else {
 730                    $GLOBALS['notification']->push(sprintf(_("Last login: %s from %s"), strftime('%c', $old_login['time']), $old_login['host']), 'horde.message');
 731                }
 732            }
 733        }
 734
 735        /* Set the user's last_login information. */
 736        $host = empty($_SERVER['HTTP_X_FORWARDED_FOR'])
 737            ? $_SERVER['REMOTE_ADDR']
 738            : $_SERVER['HTTP_X_FORWARDED_FOR'];
 739
 740	if ((@include_once 'Net/DNS.php')) {
 741	    $resolver = new Net_DNS_Resolver();
 742	    $resolver->retry = isset($GLOBALS['conf']['dns']['retry']) ? $GLOBALS['conf']['dns']['retry'] : 1;
 743	    $resolver->retrans = isset($GLOBALS['conf']['dns']['retrans']) ? $GLOBALS['conf']['dns']['retrans'] : 1;
 744	    $response = $resolver->query($host, 'PTR');
 745	    $ptrdname = $response ? $response->answer[0]->ptrdname : $host;
 746	} else {
 747	    $ptrdname = @gethostbyaddr($host);
 748	}
 749
 750        $last_login = array('time' => time(),
 751                            'host' => $ptrdname);
 752        $GLOBALS['prefs']->setValue('last_login', serialize($last_login));
 753
 754        if ($changeRequested) {
 755            $GLOBALS['notification']->push(_("Your password has expired."),
 756                                           'horde.message');
 757            if ($this->hasCapability('update')) {
 758                /* A bit of a kludge.  URL is set from the login screen, but
 759                 * we aren't completely certain we got here from the login
 760                 * screen.  So any screen which calls setAuth() which has a
 761                 * url will end up going there.  Should be OK. */
 762                $url_param = Util::getFormData('url');
 763
 764                if ($url_param) {
 765                    $url = Horde::url(Util::removeParameter($url_param,
 766                                                            session_name()),
 767                                      true);
 768                    $return_to = $GLOBALS['registry']->get('webroot', 'horde') .
 769                                 '/index.php';
 770                    $return_to = Util::addParameter($return_to, 'url', $url);
 771                } else {
 772                    $return_to = Horde::url($GLOBALS['registry']->get('webroot', 'horde')
 773                                 . '/index.php');
 774                }
 775
 776                $url = Horde::applicationUrl('services/changepassword.php');
 777                $url = Util::addParameter($url,
 778                                          array('return_to' => $return_to),
 779                                          null, false);
 780
 781                header('Location: ' . $url);
 782                exit;
 783            }
 784        }
 785
 786        return true;
 787    }
 788
 789    /**
 790     * Clears any authentication tokens in the current session.
 791     *
 792     * @param string $realm  The authentication realm to clear.
 793     */
 794    function clearAuth($realm = null)
 795    {
 796        if (!empty($realm) && isset($_SESSION['__auth'][$realm])) {
 797            $_SESSION['__auth'][$realm] = array();
 798            $_SESSION['__auth'][$realm]['authenticated'] = false;
 799        } elseif (isset($_SESSION['__auth'])) {
 800            $_SESSION['__auth'] = array();
 801            $_SESSION['__auth']['authenticated'] = false;
 802        }
 803
 804        /* Remove the user's cached preferences if they are present. */
 805        if (isset($GLOBALS['registry'])) {
 806            $GLOBALS['registry']->unloadPrefs();
 807        }
 808    }
 809
 810    /**
 811     * Is the current user an administrator?
 812     *
 813     * @param string $permission  Allow users with this permission admin access
 814     *                            in the current context.
 815     * @param integer $permlevel  The level of permissions to check for
 816     *                            (PERMS_EDIT, PERMS_DELETE, etc). Defaults
 817     *                            to PERMS_EDIT.
 818     * @param string $user        The user to check. Defaults to Auth::getAuth().
 819     *
 820     * @return boolean  Whether or not this is an admin user.
 821     */
 822    function isAdmin($permission = null, $permlevel = null, $user = null)
 823    {
 824        if (is_null($user)) {
 825            $user = Auth::getAuth();
 826        }
 827
 828        if ($user
 829            && @is_array($GLOBALS['conf']['auth']['admins'])
 830            && in_array($user, $GLOBALS['conf']['auth']['admins'])) {
 831            return true;
 832        }
 833
 834        if ($user) {
 835            $auth = &Auth::singleton($GLOBALS['conf']['auth']['driver']);
 836            if ($auth->hasCapability('admins')
 837                && $auth->_isAdmin($permission, $permlevel, $user)) {
 838                return true;
 839            }
 840        }
 841
 842        if (!is_null($permission)) {
 843            if (is_null($permlevel)) {
 844                $permlevel = PERMS_EDIT;
 845            }
 846            return $GLOBALS['perms']->hasPermission($permission, $user, $permlevel);
 847        }
 848
 849        return false;
 850    }
 851
 852    /**
 853     * Applies a hook defined by the function _username_hook_frombackend() to
 854     * the given user name if this function exists and user hooks are enabled.
 855     *
 856     * This method should be called if a authentication backend's user name
 857     * needs to be converted to a (unique) Horde user name. The backend's user
 858     * name is what the user sees and uses, but internally we use the Horde
 859     * user name.
 860     *
 861     * @param string $userId  The authentication backend's user name.
 862     *
 863     * @return string  The internal Horde user name.
 864     */
 865    function addHook($userId)
 866    {
 867        if (!empty($GLOBALS['conf']['hooks']['username'])) {
 868            $newId = Horde::callHook('_username_hook_frombackend', array($userId));
 869            if (!is_a($newId, 'PEAR_Error')) {
 870                return $newId;
 871            }
 872        }
 873
 874        return $userId;
 875    }
 876
 877    /**
 878     * Applies a hook defined by the function _username_hook_tobackend() to
 879     * the given user name if this function exists and user hooks are enabled.
 880     *
 881     * This method should be called if a Horde user name needs to be converted
 882     * to an authentication backend's user name or displayed to the user. The
 883     * backend's user name is what the user sees and uses, but internally we
 884     * use the Horde user name.
 885     *
 886     * @param string $userId  The internal Horde user name.
 887     *
 888     * @return string  The authentication backend's user name.
 889     */
 890    function removeHook($userId)
 891    {
 892        if (!empty($GLOBALS['conf']['hooks']['username'])) {
 893            $newId = Horde::callHook('_username_hook_tobackend', array($userId));
 894            if (!is_a($newId, 'PEAR_Error')) {
 895                return $newId;
 896            }
 897        }
 898
 899        return $userId;
 900    }
 901
 902    /**
 903     * Queries the current Auth object to find out if it supports the given
 904     * capability.
 905     *
 906     * @param string $capability  The capability to test for.
 907     *
 908     * @return boolean  Whether or not the capability is supported.
 909     */
 910    function hasCapability($capability)
 911    {
 912        return !empty($this->capabilities[$capability]);
 913    }
 914
 915    /**
 916     * Returns the URI of the login screen for the current authentication
 917     * method.
 918     *
 919     * @param string $app  The application to use.
 920     * @param string $url  The URL to redirect to after login.
 921     *
 922     * @return string  The login screen URI.
 923     */
 924    function getLoginScreen($app = 'horde', $url = '')
 925    {
 926        $auth = &Auth::singleton($GLOBALS['conf']['auth']['driver']);
 927        return $auth->_getLoginScreen($app, $url);
 928    }
 929
 930    /**
 931     * Returns the named parameter for the current auth driver.
 932     *
 933     * @param string $param  The parameter to fetch.
 934     *
 935     * @return string  The parameter's value.
 936     */
 937    function getParam($param)
 938    {
 939        return isset($this->_params[$param]) ? $this->_params[$param] : null;
 940    }
 941
 942    /**
 943     * Returns the name of the authentication provider.
 944     *
 945     * @param string $driver  Used by recursive calls when untangling composite
 946     *                        auth.
 947     * @param array  $params  Used by recursive calls when untangling composite
 948     *                        auth.
 949     *
 950     * @return string  The name of the driver currently providing
 951     *                 authentication.
 952     */
 953    function getProvider($driver = null, $params = null)
 954    {
 955        if (is_null($driver)) {
 956            $driver = $GLOBALS['conf']['auth']['driver'];
 957        }
 958        if (is_null($params)) {
 959            $params = Horde::getDriverConfig('auth',
 960                is_array($driver) ? $driver[1] : $driver);
 961        }
 962
 963        if ($driver == 'application') {
 964            return isset($params['app']) ? $params['app'] : 'application';
 965        } elseif ($driver == 'composite') {
 966            if (($login_driver = Auth::_getDriverByParam('loginscreen_switch', $params)) &&
 967                !empty($params['drivers'][$login_driver])) {
 968                return Auth::getProvider($params['drivers'][$login_driver]['driver'],
 969                                         isset($params['drivers'][$login_driver]['params']) ? $params['drivers'][$login_driver]['params'] : null);
 970            }
 971            return 'composite';
 972        } else {
 973            return $driver;
 974        }
 975    }
 976
 977    /**
 978     * Returns the logout reason.
 979     *
 980     * @return string One of the logout reasons (see the AUTH_LOGOUT_*
 981     *                constants for the valid reasons).  Returns null if there
 982     *                is no logout reason present.
 983     */
 984    function getLogoutReason()
 985    {
 986        if (isset($GLOBALS['__autherror']['type'])) {
 987            return $GLOBALS['__autherror']['type'];
 988        } else {
 989            return Util::getFormData(AUTH_REASON_PARAM);
 990        }
 991    }
 992
 993    /**
 994     * Returns the status string to use for logout messages.
 995     *
 996     * @return string  The logout reason string.
 997     */
 998    function getLogoutReasonString()
 999    {
1000        switch (Auth::getLogoutReason()) {
1001        case AUTH_REASON_SESSION:
1002            $text = sprintf(_("Your %s session has expired. Please login again."), $GLOBALS['registry']->get('name'));
1003            break;
1004
1005        case AUTH_REASON_SESSIONIP:
1006            $text = sprintf(_("Your Internet Address has changed since the beginning of your %s session. To protect your security, you must login again."), $GLOBALS['registry']->get('name'));
1007            break;
1008
1009        case AUTH_REASON_BROWSER:
1010            $text = sprintf(_("Your browser appears to have changed since the beginning of your %s session. To protect your security, you must login again."), $GLOBALS['registry']->get('name'));
1011            break;
1012
1013        case AUTH_REASON_LOGOUT:
1014            $text = _("You have been logged out.");
1015            break;
1016
1017        case AUTH_REASON_FAILED:
1018            $text = _("Login failed.");
1019            break;
1020
1021        case AUTH_REASON_BADLOGIN:
1022            $text = _("Login failed because your username or password was entered incorrectly.");
1023            break;
1024
1025        case AUTH_REASON_EXPIRED:
1026            $text = _("Your login has expired.");
1027            break;
1028
1029        case AUTH_REASON_MESSAGE:
1030            if (isset($GLOBALS['__autherror']['msg'])) {
1031                $text = $GLOBALS['__autherror']['msg'];
1032            } else {
1033                $text = Util::getFormData(AUTH_REASON_MSG_PARAM);
1034            }
1035            break;
1036
1037        default:
1038            $text = '';
1039            break;
1040        }
1041
1042        return $text;
1043    }
1044
1045    /**
1046     * Generates the correct parameters to pass to the given logout URL.
1047     *
1048     * If no reason/msg is passed in, use the current global authentication
1049     * error message.
1050     *
1051     * @param string $url     The URL to redirect to.
1052     * @param string $reason  The reason for logout.
1053     * @param string $msg     If reason is AUTH_REASON_MESSAGE, the message to
1054     *                        display to the user.
1055     * @return string The formatted URL
1056     */
1057    function addLogoutParameters($url, $reason = null, $msg = null)
1058    {
1059        $params = array('horde_logout_token' => Horde::getRequestToken('horde.logout'));
1060
1061        if (isset($GLOBALS['registry'])) {
1062            $params['app'] = $GLOBALS['registry']->getApp();
1063        }
1064
1065        if (is_null($reason)) {
1066            $reason = Auth::getLogoutReason();
1067        }
1068
1069        if ($reason) {
1070            $params[AUTH_REASON_PARAM] = $reason;
1071            if ($reason == AUTH_REASON_MESSAGE) {
1072                if (is_null($msg)) {
1073                    $msg = Auth::getLogoutReasonString();
1074                }
1075                $params[AUTH_REASON_MSG_PARAM] = $msg;
1076            }
1077        }
1078
1079        return Util::addParameter($url, $params, null, false);
1080    }
1081
1082    /**
1083     * Reads session data to determine if it contains Horde authentication
1084     * credentials.
1085     *
1086     * @since Horde 3.2
1087     *
1088     * @param string $session_data  The session data.
1089     * @param boolean $info         Return session information.  The following
1090     *                              information is returned: userid, realm,
1091     *                              timestamp, remote_addr, browser.
1092     *
1093     * @return array  An array of the user's sesion information if
1094     *                authenticated or false.  The following information is
1095     *                returned: userid, realm, timestamp, remote_addr, browser.
1096     */
1097    function readSessionData($session_data)
1098    {
1099        if (empty($session_data)) {
1100            return false;
1101        }
1102
1103        $pos = strpos($session_data, '__auth|');
1104        if ($pos === false) {
1105            return false;
1106        }
1107
1108        $endpos = $pos + 7;
1109        $old_error = error_reporting(0);
1110
1111        while ($endpos !== false) {
1112            $endpos = strpos($session_data, '|', $endpos);
1113            $data = unserialize(substr($session_data, $pos + 7, $endpos));
1114            if (is_array($data)) {
1115                error_reporting($old_error);
1116                if (empty($data['authenticated'])) {
1117                    return false;
1118                }
1119                return array(
1120                    'userid' => $data['userId'],
1121                    'realm' => $data['realm'],
1122                    'timestamp' => $data['timestamp'],
1123                    'remote_addr' => $data['remote_addr'],
1124                    'browser' => $data['browser']
1125                );
1126            }
1127            ++$endpos;
1128        }
1129
1130        return false;
1131    }
1132
1133    /**
1134     * Returns the URI of the login screen for this authentication object.
1135     *
1136     * @access private
1137     *
1138     * @param string $app  The application to use.
1139     * @param string $url  The URL to redirect to after login.
1140     *
1141     * @return string  The login screen URI.
1142     */
1143    function _getLoginScreen($app = 'horde', $url = '')
1144    {
1145        $login = Horde::url($GLOBALS['registry']->get('webroot', $app) . '/login.php', true);
1146        if (!empty($url)) {
1147            $login = Util::addParameter($login, 'url', $url);
1148        }
1149        return $login;
1150    }
1151
1152    /**
1153     * Authentication stub.
1154     *
1155     * @abstract
1156     * @access protected
1157     *
1158     * @return boolean  False.
1159     */
1160    function _authenticate()
1161    {
1162        return false;
1163    }
1164
1165    /**
1166     * Driver-level admin check stub.
1167     *
1168     * @abstract
1169     * @access protected
1170     *
1171     * @return boolean  False.
1172     */
1173    function _isAdmin($permission = null, $permlevel = null, $user = null)
1174    {
1175        return false;
1176    }
1177
1178    /**
1179     * Sets the error message for an invalid authentication.
1180     *
1181     * @access private
1182     *
1183     * @param string $type  The type of error (AUTH_REASON constant).
1184     * @param string $msg   The error message/reason for invalid
1185     *                      authentication.
1186     */
1187    function _setAuthError($type, $msg = null)
1188    {
1189        $GLOBALS['__autherror'] = array();
1190        $GLOBALS['__autherror']['type'] = $type;
1191        $GLOBALS['__autherror']['msg'] = $msg;
1192    }
1193
1194    /**
1195     * Returns the error type for an invalid authentication or false on error.
1196     *
1197     * @access private
1198     *
1199     * @return mixed Error type or false on error
1200     */
1201    function _getAuthError()
1202    {
1203        if (isset($GLOBALS['__autherror']['type'])) {
1204            return $GLOBALS['__autherror']['type'];
1205        }
1206
1207        return false;
1208    }
1209
1210    /**
1211     * Returns the appropriate authentication driver, if any, selecting by the
1212     * specified parameter.
1213     *
1214     * @access private
1215     *
1216     * @param string $name          The parameter name.
1217     * @param array $params         The parameter list.
1218     * @param string $driverparams  A list of parameters to pass to the driver.
1219     *
1220     * @return mixed Return value or called user func or null if unavailable
1221     */
1222    function _getDriverByParam($name, $params, $driverparams = array())
1223    {
1224        if (isset($params[$name]) &&
1225            function_exists($params[$name])) {
1226            return call_user_func_array($params[$name], $driverparams);
1227        }
1228
1229        return null;
1230    }
1231
1232    /**
1233     * Performs check on session to see if IP Address has changed since the
1234     * last access.
1235     *
1236     * @access private
1237     *
1238     * @return boolean  True if IP Address is the same (or the check is
1239     *                  disabled), false if the address has changed.
1240     */
1241    function _checkSessionIP()
1242    {
1243        return (empty($GLOBALS['conf']['auth']['checkip']) ||
1244                (isset($_SESSION['__auth']['remote_addr']) && $_SESSION['__auth']['remote_addr'] == $_SERVER['REMOTE_ADDR']));
1245    }
1246
1247    /**
1248     * Performs check on session to see if browser string has changed since
1249     * the last access.
1250     *
1251     * @access private
1252     *
1253     * @return boolean  True if browser string is the same, false if the
1254     *                  string has changed.
1255     */
1256    function _checkBrowserString()
1257    {
1258        return (empty($GLOBALS['conf']['auth']['checkbrowser']) ||
1259                $_SESSION['__auth']['browser'] == $GLOBALS['browser']->getAgentString());
1260    }
1261
1262    /**
1263     * Converts to allowed 64 characters for APRMD5 passwords.
1264     *
1265     * @access private
1266     *
1267     * @param string  $value
1268     * @param integer $count
1269     *
1270     * @return string  $value converted to the 64 MD5 characters.
1271     */
1272    function _toAPRMD5($value, $count)
1273    {
1274        /* 64 characters that are valid for APRMD5 passwords. */
1275        $APRMD5 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
1276
1277        $aprmd5 = '';
1278        $count = abs($count);
1279        while (--$count) {
1280            $aprmd5 .= $APRMD5[$value & 0x3f];
1281            $value >>= 6;
1282        }
1283        return $aprmd5;
1284    }
1285
1286    /**
1287     * Attempts to return a concrete Auth instance based on $driver.
1288     *
1289     * @param mixed $driver  The type of concrete Auth subclass to return. This
1290     *                       is based on the storage driver ($driver). The code
1291     *                       is dynamically included. If $driver is an array,
1292     *                       then we will look in $driver[0]/lib/Auth/ for the
1293     *                       subclass implementation named $driver[1].php.
1294     * @param array $params  A hash containing any additional configuration or
1295     *                       connection parameters a subclass might need.
1296     *
1297     * @return Auth  The newly created concrete Auth instance, or false on an
1298     *               error.
1299     */
1300    function factory($driver, $params = null)
1301    {
1302        if (is_array($driver)) {
1303            $app = $driver[0];
1304            $driver = $driver[1];
1305        }
1306
1307        $driver = basename($driver);
1308        if (empty($driver) || ($driver == 'none')) {
1309            return new Auth();
1310        }
1311
1312        if (is_null($params)) {
1313            $params = Horde::getDriverConfig('auth', $driver);
1314        }
1315
1316        $class = 'Auth_' . $driver;
1317        $include_error = '';
1318        if (!class_exists($class)) {
1319            $oldTrackErrors = ini_set('track_errors', 1);
1320            if (!empty($app)) {
1321                include $GLOBALS['registry']->get('fileroot', $app) . '/lib/Auth/' . $driver . '.php';
1322            } else {
1323                include 'Horde/Auth/' . $driver . '.php';
1324            }
1325            if (isset($php_errormsg)) {
1326                $include_error = $php_errormsg;
1327            }
1328            ini_set('track_errors', $oldTrackErrors);
1329        }
1330
1331        if (class_exists($class)) {
1332            $auth = new $class($params);
1333        } else {
1334            $auth = PEAR::raiseError('Auth Driver (' . $class . ') not found' . ($include_error ? ': ' . $include_error : '') . '.');
1335        }
1336
1337        return $auth;
1338    }
1339
1340    /**
1341     * Attempts to return a reference to a concrete Auth instance based on
1342     * $driver. It will only create a new instance if no Auth instance with
1343     * the same parameters currently exists.
1344     *
1345     * This should be used if multiple authentication sources (and, thus,
1346     * multiple Auth instances) are required.
1347     *
1348     * This method must be invoked as: $var = &Auth::singleton()
1349     *
1350     * @param string $driver  The type of concrete Auth subclass to return.
1351     *                        This is based on the storage driver ($driver).
1352     *                        The code is dynamically included.
1353     * @param array $params   A hash containing any additional configuration or
1354     *                        connection parameters a subclass might need.
1355     *
1356     * @return Auth  The concrete Auth reference, or false on an error.
1357     */
1358    function &singleton($driver, $params = null)
1359    {
1360        static $instances = array();
1361
1362        if (is_null($params)) {
1363            $params = Horde::getDriverConfig('auth',
1364                is_array($driver) ? $driver[1] : $driver);
1365        }
1366
1367        $signature = serialize(array($driver, $params));
1368        if (empty($instances[$signature])) {
1369            $instances[$signature] = Auth::factory($driver, $params);
1370        }
1371
1372        return $instances[$signature];
1373    }
1374
1375}