1<?php
2
3require_once(INCLUDE_DIR.'class.2fa.php');
4
5interface AuthenticatedUser {
6    // Get basic information
7    function getId();
8    function getUsername();
9    function getUserType();
10
11
12    // Get password reset timestamp
13    function getPasswdResetTimestamp();
14
15    //Backend used to authenticate the user
16    function getAuthBackend();
17
18    // Get 2FA Backend
19    function get2FABackend();
20
21    //Authentication key
22    function setAuthKey($key);
23
24    function getAuthKey();
25
26    // logOut the user
27    function logOut();
28
29    // Signal method to allow performing extra things when a user is logged
30    // into the sysem
31    function onLogin($bk);
32}
33
34abstract class BaseAuthenticatedUser
35implements AuthenticatedUser {
36    //Authorization key returned by the backend used to authorize the user
37    private $authkey;
38
39    // Get basic information
40    abstract function getId();
41    abstract function getUsername();
42    abstract function getUserType();
43
44    // Get password reset timestamp
45    function getPasswdResetTimestamp() {
46        return null;
47    }
48
49    //Backend used to authenticate the user
50    abstract function getAuthBackend();
51
52    // Get 2FA Backend
53    abstract function get2FABackend();
54
55    //Authentication key
56    function setAuthKey($key) {
57        $this->authkey = $key;
58    }
59
60    function getAuthKey() {
61        return $this->authkey;
62    }
63
64    // logOut the user
65    function logOut() {
66
67        if ($bk = $this->getAuthBackend())
68            return $bk->signOut($this);
69
70        return false;
71    }
72
73    // Signal method to allow performing extra things when a user is logged
74    // into the sysem
75    function onLogin($bk) {}
76}
77
78require_once(INCLUDE_DIR.'class.ostsession.php');
79require_once(INCLUDE_DIR.'class.usersession.php');
80
81interface AuthDirectorySearch {
82    /**
83     * Indicates if the backend can be used to search for user information.
84     * Lookup is performed to find user information based on a unique
85     * identifier.
86     */
87    function lookup($id);
88
89    /**
90     * Indicates if the backend supports searching for usernames. This is
91     * distinct from information lookup in that lookup is intended to lookup
92     * information based on a unique identifier
93     */
94    function search($query);
95}
96
97/**
98 * Class: ClientCreateRequest
99 *
100 * Simple container to represent a remote authentication success for a
101 * client which should be imported into the local database. The class will
102 * provide access to the backend that authenticated the user, the username
103 * that the user entered when logging in, and any other information about
104 * the user that the backend was able to lookup. Generally, this extra
105 * information would be the same information retrieved from calling the
106 * AuthDirectorySearch::lookup() method.
107 */
108class ClientCreateRequest {
109
110    var $backend;
111    var $username;
112    var $info;
113
114    function __construct($backend, $username, $info=array()) {
115        $this->backend = $backend;
116        $this->username = $username;
117        $this->info = $info;
118    }
119
120    function getBackend() {
121        return $this->backend;
122    }
123    function setBackend($what) {
124        $this->backend = $what;
125    }
126
127    function getUsername() {
128        return $this->username;
129    }
130    function getInfo() {
131        return $this->info;
132    }
133
134    function attemptAutoRegister() {
135        global $cfg;
136
137        if (!$cfg || !$cfg->isClientRegistrationEnabled())
138            return false;
139
140        // Attempt to automatically register
141        $this_form = UserForm::getUserForm()->getForm($this->getInfo());
142        $bk = $this->getBackend();
143        $defaults = array(
144            'timezone' => $cfg->getDefaultTimezone(),
145            'username' => $this->getUsername(),
146        );
147        if ($bk->supportsInteractiveAuthentication())
148            // User can only be authenticated against this backend
149            $defaults['backend'] = $bk::$id;
150        if ($this_form->isValid(function($f) { return !$f->isVisibleToUsers(); })
151                && ($U = User::fromVars($this_form->getClean()))
152                && ($acct = ClientAccount::createForUser($U, $defaults))
153                // Confirm and save the account
154                && $acct->confirm()
155                // Login, since `tickets.php` will not attempt SSO
156                && ($cl = new ClientSession(new EndUser($U)))
157                && ($bk->login($cl, $bk)))
158            return $cl;
159    }
160}
161
162/**
163 * Authentication backend
164 *
165 * Authentication provides the basis of abstracting the link between the
166 * login page with a username and password and the staff member,
167 * administrator, or client using the system.
168 *
169 * The system works by allowing the AUTH_BACKENDS setting from
170 * ost-config.php to determine the list of authentication backends or
171 * providers and also specify the order they should be evaluated in.
172 *
173 * The authentication backend should define a authenticate() method which
174 * receives a username and optional password. If the authentication
175 * succeeds, an instance deriving from <User> should be returned.
176 */
177abstract class AuthenticationBackend {
178    static protected $registry = array();
179    static $name;
180    static $id;
181
182
183    /* static */
184    static function register($class) {
185        if (is_string($class) && class_exists($class))
186            $class = new $class();
187
188        if (!is_object($class)
189                || !($class instanceof AuthenticationBackend))
190            return false;
191
192        return static::_register($class);
193    }
194
195    static function _register($class) {
196        // XXX: Raise error if $class::id is already in the registry
197        static::$registry[$class::$id] = $class;
198    }
199
200    static function allRegistered() {
201        return static::$registry;
202    }
203
204    static function getBackend($id) {
205
206        if ($id
207                && ($backends = static::allRegistered())
208                && isset($backends[$id]))
209            return $backends[$id];
210    }
211
212    static function getSearchDirectoryBackend($id) {
213
214        if ($id
215                && ($backends = static::getSearchDirectories())
216                && isset($backends[$id]))
217            return $backends[$id];
218    }
219
220    /*
221     * Allow the backend to do login audit depending on the result
222     * This is mainly used to track failed login attempts
223     */
224    static function authAudit($result, $credentials=null) {
225
226        if (!$result) return;
227
228        foreach (static::allRegistered() as $bk)
229            $bk->audit($result, $credentials);
230    }
231
232    static function process($username, $password=null, &$errors=array()) {
233
234        if (!$username)
235            return false;
236
237        $backends =  static::getAllowedBackends($username);
238        foreach (static::allRegistered() as $bk) {
239            if ($backends //Allowed backends
240                    && $bk->supportsInteractiveAuthentication()
241                    && !in_array($bk::$id, $backends))
242                // User cannot be authenticated against this backend
243                continue;
244
245            // All backends are queried here, even if they don't support
246            // authentication so that extensions like lockouts and audits
247            // can be supported.
248            try {
249                $result = $bk->authenticate($username, $password);
250                if ($result instanceof AuthenticatedUser
251                        && ($bk->login($result, $bk)))
252                    return $result;
253                elseif ($result instanceof ClientCreateRequest
254                        && $bk instanceof UserAuthenticationBackend)
255                    return $result;
256                elseif ($result instanceof AccessDenied) {
257                    break;
258                }
259            }
260            catch (AccessDenied $e) {
261                $result = $e;
262                break;
263            }
264        }
265
266        if (!$result)
267            $result = new AccessDenied(__('Access denied'));
268
269        if ($result && $result instanceof AccessDenied)
270            $errors['err'] = $result->reason;
271
272        $info = array('username' => $username, 'password' => $password);
273        Signal::send('auth.login.failed', null, $info);
274        self::authAudit($result, $info);
275    }
276
277    /*
278     *  Attempt to process non-interactive sign-on e.g  HTTP-Passthrough
279     *
280     * $forcedAuth - indicate if authentication is required.
281     *
282     */
283    function processSignOn(&$errors, $forcedAuth=true) {
284
285        foreach (static::allRegistered() as $bk) {
286            // All backends are queried here, even if they don't support
287            // authentication so that extensions like lockouts and audits
288            // can be supported.
289            try {
290                $result = $bk->signOn();
291                if ($result instanceof AuthenticatedUser) {
292                    //Perform further Object specific checks and the actual login
293                    if (!$bk->login($result, $bk))
294                        continue;
295
296                    return $result;
297                }
298                elseif ($result instanceof ClientCreateRequest
299                        && $bk instanceof UserAuthenticationBackend)
300                    return $result;
301                elseif ($result instanceof AccessDenied) {
302                    break;
303                }
304            }
305            catch (AccessDenied $e) {
306                $result = $e;
307                break;
308            }
309        }
310
311        if (!$result && $forcedAuth)
312            $result = new  AccessDenied(__('Unknown user'));
313
314        if ($result && $result instanceof AccessDenied)
315            $errors['err'] = $result->reason;
316
317        self::authAudit($result);
318    }
319
320    static function getSearchDirectories() {
321        $backends = array();
322        foreach (StaffAuthenticationBackend::allRegistered() as $bk)
323            if ($bk instanceof AuthDirectorySearch)
324                $backends[$bk::$id] = $bk;
325
326        foreach (UserAuthenticationBackend::allRegistered() as $bk)
327            if ($bk instanceof AuthDirectorySearch)
328                $backends[$bk::$id] = $bk;
329
330        return array_unique($backends);
331    }
332
333    static function searchUsers($query) {
334        $users = array();
335        foreach (static::getSearchDirectories() as $bk)
336            $users = array_merge($users, $bk->search($query));
337
338        return $users;
339    }
340
341    /**
342     * Fetches the friendly name of the backend
343     */
344    function getName() {
345        return static::$name;
346    }
347
348    /**
349     * Indicates if the backed supports authentication. Useful if the
350     * backend is used for logging or lockout only
351     */
352    function supportsInteractiveAuthentication() {
353        return true;
354    }
355
356    /**
357     * Indicates if the backend supports changing a user's password. This
358     * would be done in two fashions. Either the currently-logged in user
359     * want to change its own password or a user requests to have their
360     * password reset. This requires an administrative privilege which this
361     * backend might not possess, so it's defined in supportsPasswordReset()
362     */
363    function supportsPasswordChange() {
364        return false;
365    }
366
367
368    /**
369     * Get supported password policies for the backend.
370     *
371     */
372    function getPasswordPolicies($user=null) {
373        return PasswordPolicy::allActivePolicies();
374    }
375
376    /**
377     * Request the backend to update the password for a user. This method is
378     * the main entry for password updates so that password policies can be
379     * applied to the new password before passing the new password to the
380     * backend for updating.
381     *
382     * Throws:
383     * BadPassword — if password does not meet policy requirement
384     * PasswordUpdateFailed — if backend failed to update the password
385     */
386    function setPassword($user, $password, $current=false) {
387        foreach ($this->getPasswordPolicies($user) as $P)
388            $P->onSet($password, $current);
389
390        $rv = $this->syncPassword($user, $password);
391        if ($rv) {
392            $info = array('password' => $password, 'current' => $current);
393            Signal::send('auth.pwchange', $user, $info);
394        }
395        return $rv;
396    }
397
398    /*
399     * Request the backend to check the policies for a just logged
400     * in user.
401     * Throws: BadPassword & ExpiredPassword - for password related failures
402     */
403    function checkPolicies($user, $password) {
404        // Password policies
405        foreach ($this->getPasswordPolicies($user) as $P)
406            $P->onLogin($user, $password);
407    }
408
409    /**
410     * Request the backend to update the user's password with the password
411     * given. This method should only be used if the backend advertises
412     * supported password updates with the supportsPasswordChange() method.
413     *
414     * Returns:
415     * true if the password was successfully updated and false otherwise.
416     */
417    protected function syncPassword($user, $password) {
418        return false;
419    }
420
421    function supportsPasswordReset() {
422        return false;
423    }
424
425    function signOn() {
426        return null;
427    }
428
429    protected function validate($auth) {
430        return null;
431    }
432
433    protected function audit($result, $credentials) {
434        return null;
435    }
436
437    abstract function authenticate($username, $password);
438    abstract function login($user, $bk);
439    abstract static function getUser(); //Validates  authenticated users.
440    abstract function getAllowedBackends($userid);
441    abstract protected function getAuthKey($user);
442    abstract static function signOut($user);
443}
444
445/**
446 * ExternalAuthenticationBackend
447 *
448 * External authentication backends are backends such as Google+ which
449 * require a redirect to a remote site and a redirect back to osTicket in
450 * order for a  user to be authenticated. For such backends, neither the
451 * username and password fields nor single sign on alone can be used to
452 * authenticate the user.
453 */
454interface ExternalAuthentication {
455
456    /**
457     * Requests the backend to render an external link box. When the user
458     * clicks this box, the backend will be prompted to redirect the user to
459     * the remote site for authentication there.
460     */
461    function renderExternalLink();
462
463    /**
464     * Function: getServiceName
465     *
466     * Called to get the service name displayed on login page.
467     */
468     function getServiceName();
469
470    /**
471     * Function: triggerAuth
472     *
473     * Called when a user clicks the button rendered in the
474     * ::renderExternalLink() function. This method should initiate the
475     * remote authentication mechanism.
476     */
477    function triggerAuth();
478}
479
480abstract class StaffAuthenticationBackend  extends AuthenticationBackend {
481
482    static private $_registry = array();
483
484    static function _register($class) {
485        static::$_registry[$class::$id] = $class;
486    }
487
488    static function allRegistered() {
489        return array_merge(self::$_registry, parent::allRegistered());
490    }
491
492    function isBackendAllowed($staff, $bk) {
493
494        if (!($backends=self::getAllowedBackends($staff->getId())))
495            return true;  //No restrictions
496
497        return in_array($bk::$id, array_map('strtolower', $backends));
498    }
499
500    function getPasswordPolicies($user=null) {
501        global $cfg;
502        $policies = PasswordPolicy::allActivePolicies();
503        if ($cfg && ($policy = $cfg->getStaffPasswordPolicy())) {
504            foreach ($policies as $P)
505                if ($policy == $P::$id)
506                    return array($P);
507        }
508
509        return $policies;
510    }
511
512    function getAllowedBackends($userid) {
513
514        $backends =array();
515        //XXX: Only one backend can be specified at the moment.
516        $sql = 'SELECT backend FROM '.STAFF_TABLE
517              .' WHERE backend IS NOT NULL ';
518        if (is_numeric($userid))
519            $sql.= ' AND staff_id='.db_input($userid);
520        else {
521            $sql.= ' AND (username='.db_input($userid) .' OR email='.db_input($userid).')';
522        }
523
524        if (($res=db_query($sql, false)) && db_num_rows($res))
525            $backends[] = db_result($res);
526
527        return array_filter($backends);
528    }
529
530    function login($staff, $bk) {
531        global $ost;
532
533        if (!$bk || !($staff instanceof Staff))
534            return false;
535
536        // Ensure staff is allowed for realz to be authenticated via the backend.
537        if (!static::isBackendAllowed($staff, $bk)
538            || !($authkey=$bk->getAuthKey($staff)))
539            return false;
540
541        //Log debug info.
542        $ost->logDebug(_S('Agent Login'),
543            sprintf(_S("%s logged in [%s], via %s"), $staff->getUserName(),
544                $_SERVER['REMOTE_ADDR'], get_class($bk))); //Debug.
545
546        $agent = Staff::lookup($staff->getId());
547        $type = array('type' => 'login');
548        Signal::send('person.login', $agent, $type);
549
550        // Check if the agent has 2fa enabled
551        $auth2fa = null;
552        if (($_2fa = $staff->get2FABackend())
553                && ($token=$_2fa->send($staff))) {
554            $auth2fa = sprintf('%s:%s:%s',
555                    $_2fa->getId(), md5($token.$staff->getId()), time());
556        }
557
558        // Tag the authkey.
559        $authkey = $bk::$id.':'.$authkey;
560        // Now set session crap and lets roll baby!
561        $authsession = &$_SESSION['_auth']['staff'];
562        $authsession = array(); //clear.
563        $authsession['id'] = $staff->getId();
564        $authsession['key'] =  $authkey;
565        $authsession['2fa'] =  $auth2fa;
566
567        $staff->setAuthKey($authkey);
568        $staff->refreshSession(true); //set the hash.
569        Signal::send('auth.login.succeeded', $staff);
570
571        if ($bk->supportsInteractiveAuthentication())
572            $staff->cancelResetTokens();
573
574
575        // Update last-used language, login time, etc
576        $staff->onLogin($bk);
577
578        return true;
579    }
580
581    /* Base signOut
582     *
583     * Backend should extend the signout and perform any additional signout
584     * it requires.
585     */
586
587    static function signOut($staff) {
588        global $ost;
589
590        $_SESSION['_auth']['staff'] = array();
591        unset($_SESSION[':token']['staff']);
592        $ost->logDebug(_S('Agent logout'),
593                sprintf(_S("%s logged out [%s]"),
594                    $staff->getUserName(),
595                    $_SERVER['REMOTE_ADDR'])); //Debug.
596
597        $agent = Staff::lookup($staff->getId());
598        $type = array('type' => 'logout');
599        Signal::send('person.logout', $agent, $type);
600        Signal::send('auth.logout', $staff);
601    }
602
603    // Called to get authenticated user (if any)
604    static function getUser() {
605
606        if (!isset($_SESSION['_auth']['staff'])
607                || !$_SESSION['_auth']['staff']['key'])
608            return null;
609
610        list($id, $auth) = explode(':', $_SESSION['_auth']['staff']['key']);
611
612        if (!($bk=static::getBackend($id)) //get the backend
613                || !($staff = $bk->validate($auth)) //Get AuthicatedUser
614                || !($staff instanceof Staff)
615                || $staff->getId() != $_SESSION['_auth']['staff']['id'] // check ID
616        )
617            return null;
618
619        $staff->setAuthKey($_SESSION['_auth']['staff']['key']);
620
621        return $staff;
622    }
623
624    function authenticate($username, $password) {
625        return false;
626    }
627
628    // Generic authentication key for staff's backend is the username
629    protected function getAuthKey($staff) {
630
631        if(!($staff instanceof Staff))
632            return null;
633
634        return $staff->getUsername();
635    }
636
637    protected function validate($authkey) {
638
639        if (($staff = StaffSession::lookup($authkey)) && $staff->getId())
640            return $staff;
641    }
642}
643
644abstract class ExternalStaffAuthenticationBackend
645        extends StaffAuthenticationBackend
646        implements ExternalAuthentication {
647
648    static $fa_icon = "signin";
649    static $sign_in_image_url = false;
650    static $service_name = "External";
651
652    function getServiceName() {
653        return __(static::$service_name);
654    }
655
656    function renderExternalLink() {
657        $service = sprintf('%s %s',
658                __('Sign in with'),
659                $this->getServiceName());
660        ?>
661        <a class="external-sign-in" title="<?php echo $service; ?>"
662                href="login.php?do=ext&amp;bk=<?php echo urlencode(static::$id); ?>">
663<?php if (static::$sign_in_image_url) { ?>
664        <img class="sign-in-image" src="<?php echo static::$sign_in_image_url;
665            ?>" alt="<?php echo $service; ?>"/>
666<?php } else { ?>
667            <div class="external-auth-box">
668            <span class="external-auth-icon">
669                <i class="icon-<?php echo static::$fa_icon; ?> icon-large icon-fixed-with"></i>
670            </span>
671            <span class="external-auth-name">
672               <?php echo $service; ?>
673            </span>
674            </div>
675<?php } ?>
676        </a><?php
677    }
678
679    function triggerAuth() {
680        $_SESSION['ext:bk:class'] = get_class($this);
681    }
682}
683Signal::connect('api', function($dispatcher) {
684    $dispatcher->append(
685        url('^/auth/ext$', function() {
686            if ($class = $_SESSION['ext:bk:class']) {
687                $bk = StaffAuthenticationBackend::getBackend($class::$id)
688                    ?: UserAuthenticationBackend::getBackend($class::$id);
689                if ($bk instanceof ExternalAuthentication)
690                    $bk->triggerAuth();
691            }
692        })
693    );
694});
695
696abstract class UserAuthenticationBackend  extends AuthenticationBackend {
697
698    static private $_registry = array();
699
700    static function _register($class) {
701        static::$_registry[$class::$id] = $class;
702    }
703
704    static function allRegistered() {
705        return array_merge(self::$_registry, parent::allRegistered());
706    }
707
708
709    function getPasswordPolicies($user=null) {
710        global $cfg;
711        $policies = PasswordPolicy::allActivePolicies();
712        if ($cfg && ($policy = $cfg->getClientPasswordPolicy())) {
713            foreach ($policies as $P)
714                if ($policy == $P::$id)
715                    return array($P);
716        }
717
718        return $policies;
719    }
720
721    function getAllowedBackends($userid) {
722        $backends = array();
723        $sql = 'SELECT A1.backend FROM '.USER_ACCOUNT_TABLE
724              .' A1 INNER JOIN '.USER_EMAIL_TABLE.' A2 ON (A2.user_id = A1.user_id)'
725              .' WHERE backend IS NOT NULL '
726              .' AND (A1.username='.db_input($userid)
727                  .' OR A2.`address`='.db_input($userid).')';
728
729        if (!($res=db_query($sql, false)))
730            return $backends;
731
732        while (list($bk) = db_fetch_row($res))
733            $backends[] = $bk;
734
735        return array_filter($backends);
736    }
737
738    function login($user, $bk) {
739        global $ost;
740
741        if (!$user || !$bk
742                || !$bk::$id //Must have ID
743                || !($authkey = $bk->getAuthKey($user)))
744            return false;
745
746        $acct = $user->getAccount();
747
748        if ($acct) {
749            if (!$acct->isConfirmed())
750                throw new AccessDenied(__('Account confirmation required'));
751            elseif ($acct->isLocked())
752                throw new AccessDenied(__('Account is administratively locked'));
753        }
754
755        // Tag the user and associated ticket in the SESSION
756        $this->setAuthKey($user, $bk, $authkey);
757
758        //The backend used decides the format of the auth key.
759        // XXX: encrypt to hide the bk??
760        $user->setAuthKey($authkey);
761
762        $user->refreshSession(true); //set the hash.
763
764        //Log login info...
765        $msg=sprintf(_S('%1$s (%2$s) logged in [%3$s]'
766                /* Tokens are <username>, <id>, and <ip> */),
767                $user->getUserName(), $user->getId(), $_SERVER['REMOTE_ADDR']);
768        $ost->logDebug(_S('User login'), $msg);
769
770        $u = $user->getSessionUser()->getUser();
771        $type = array('type' => 'login');
772        Signal::send('person.login', $u, $type);
773
774        if ($bk->supportsInteractiveAuthentication() && ($acct=$user->getAccount()))
775            $acct->cancelResetTokens();
776
777        // Update last-used language, login time, etc
778        $user->onLogin($bk);
779
780        return true;
781    }
782
783    function setAuthKey($user, $bk, $key=false) {
784        $authkey = $key ?: $bk->getAuthKey($user);
785
786        //Tag the authkey.
787        $authkey = $bk::$id.':'.$authkey;
788
789        //Set the session goodies
790        $authsession = &$_SESSION['_auth']['user'];
791
792        $authsession = array(); //clear.
793        $authsession['id'] = $user->getId();
794        $authsession['key'] = $authkey;
795    }
796
797    function authenticate($username, $password) {
798        return false;
799    }
800
801    static function signOut($user) {
802        global $ost;
803
804        $_SESSION['_auth']['user'] = array();
805        unset($_SESSION[':token']['client']);
806        $ost->logDebug(_S('User logout'),
807            sprintf(_S("%s logged out [%s]" /* Tokens are <username> and <ip> */),
808                $user->getUserName(), $_SERVER['REMOTE_ADDR']));
809
810        $u = $user->getSessionUser()->getUser();
811        $type = array('type' => 'logout');
812        Signal::send('person.logout', $u, $type);
813    }
814
815    protected function getAuthKey($user) {
816        return  $user->getId();
817    }
818
819    static function getUser() {
820
821        if (!isset($_SESSION['_auth']['user'])
822                || !$_SESSION['_auth']['user']['key'])
823            return null;
824
825        list($id, $auth) = explode(':', $_SESSION['_auth']['user']['key']);
826
827        if (!($bk=static::getBackend($id)) //get the backend
828                || !($user=$bk->validate($auth)) //Get AuthicatedUser
829                || !($user instanceof AuthenticatedUser) // Make sure it user
830                || $user->getId() != $_SESSION['_auth']['user']['id'] // check ID
831                )
832            return null;
833
834        $user->setAuthKey($_SESSION['_auth']['user']['key']);
835
836        return $user;
837    }
838
839    protected function validate($userid) {
840        if (!($user = User::lookup($userid)))
841            return false;
842        elseif (!$user->getAccount())
843            return false;
844
845        return new ClientSession(new EndUser($user));
846    }
847}
848
849abstract class ExternalUserAuthenticationBackend
850        extends UserAuthenticationBackend
851        implements ExternalAuthentication {
852
853    static $fa_icon = "signin";
854    static $sign_in_image_url = false;
855    static $service_name = "External";
856
857    function getServiceName() {
858        return __(static::$service_name);
859    }
860
861    function renderExternalLink() {
862        $service = sprintf('%s %s',
863                __('Sign in with'),
864                $this->getServiceName());
865
866        ?>
867        <a class="external-sign-in" title="<?php echo $service; ?>"
868                href="login.php?do=ext&amp;bk=<?php echo urlencode(static::$id); ?>">
869<?php if (static::$sign_in_image_url) { ?>
870        <img class="sign-in-image" src="<?php echo static::$sign_in_image_url;
871            ?>" alt="<?php $service; ?>"/>
872<?php } else { ?>
873            <div class="external-auth-box">
874            <span class="external-auth-icon">
875                <i class="icon-<?php echo static::$fa_icon; ?> icon-large icon-fixed-with"></i>
876            </span>
877            <span class="external-auth-name">
878                <?php echo $service; ?>
879            </span>
880            </div>
881<?php } ?>
882        </a><?php
883    }
884
885    function triggerAuth() {
886        $_SESSION['ext:bk:class'] = get_class($this);
887    }
888}
889
890/**
891 * This will be an exception in later versions of PHP
892 */
893class AccessDenied extends Exception {
894    function __construct($reason) {
895        $this->reason = $reason;
896        parent::__construct($reason);
897    }
898}
899
900/**
901 * Simple authentication backend which will lock the login form after a
902 * configurable number of attempts
903 */
904abstract class AuthStrikeBackend extends AuthenticationBackend {
905
906    function authenticate($username, $password=null) {
907        return static::authTimeout();
908    }
909
910    function signOn() {
911        return static::authTimeout();
912    }
913
914    static function signOut($user) {
915        return false;
916    }
917
918
919    function login($user, $bk) {
920        return false;
921    }
922
923    static function getUser() {
924        return null;
925    }
926
927    function supportsInteractiveAuthentication() {
928        return false;
929    }
930
931    function getAllowedBackends($userid) {
932        return array();
933    }
934
935    function getAuthKey($user) {
936        return null;
937    }
938
939    //Provides audit facility for logins attempts
940    function audit($result, $credentials) {
941
942        //Count failed login attempts as a strike.
943        if ($result instanceof AccessDenied)
944            return static::authStrike($credentials);
945
946    }
947
948    abstract function authStrike($credentials);
949    abstract function authTimeout();
950}
951
952/*
953 * Backend to monitor staff's failed login attempts
954 */
955class StaffAuthStrikeBackend extends  AuthStrikeBackend {
956
957    function authTimeout() {
958        global $ost;
959
960        $cfg = $ost->getConfig();
961
962        $authsession = &$_SESSION['_auth']['staff'];
963        if (!$authsession['laststrike'])
964            return;
965
966        //Veto login due to excessive login attempts.
967        if((time()-$authsession['laststrike'])<$cfg->getStaffLoginTimeout()) {
968            $authsession['laststrike'] = time(); //reset timer.
969            return new AccessDenied(__('Maximum failed login attempts reached'));
970        }
971
972        //Timeout is over.
973        //Reset the counter for next round of attempts after the timeout.
974        $authsession['laststrike']=null;
975        $authsession['strikes']=0;
976    }
977
978    function authstrike($credentials) {
979        global $ost;
980
981        $cfg = $ost->getConfig();
982
983        $authsession = &$_SESSION['_auth']['staff'];
984
985        $username = $credentials['username'];
986
987        $authsession['strikes']+=1;
988        if($authsession['strikes']>$cfg->getStaffMaxLogins()) {
989            $authsession['laststrike']=time();
990            $timeout = $cfg->getStaffLoginTimeout()/60;
991            $alert=_S('Excessive login attempts by an agent?')."\n"
992                   ._S('Username').": $username\n"
993                   ._S('IP').": {$_SERVER['REMOTE_ADDR']}\n"
994                   ._S('Time').": ".date('M j, Y, g:i a T')."\n\n"
995                   ._S('Attempts').": {$authsession['strikes']}\n"
996                   ._S('Timeout').": ".sprintf(_N('%d minute', '%d minutes', $timeout), $timeout)."\n\n";
997            $admin_alert = ($cfg->alertONLoginError() == 1) ? TRUE : FALSE;
998            $ost->logWarning(sprintf(_S('Excessive login attempts (%s)'),$username),
999                    $alert, $admin_alert);
1000
1001              if ($username) {
1002                $agent = Staff::lookup($username);
1003                $type = array('type' => 'login', 'msg' => sprintf('Excessive login attempts (%s)', $authsession['strikes']));
1004                Signal::send('person.login', $agent, $type);
1005              }
1006
1007            return new AccessDenied(__('Forgot your login info? Contact Admin.'));
1008        //Log every other third failed login attempt as a warning.
1009        } elseif($authsession['strikes']%3==0) {
1010            $alert=_S('Username').": {$username}\n"
1011                    ._S('IP').": {$_SERVER['REMOTE_ADDR']}\n"
1012                    ._S('Time').": ".date('M j, Y, g:i a T')."\n\n"
1013                    ._S('Attempts').": {$authsession['strikes']}";
1014            $ost->logWarning(sprintf(_S('Failed agent login attempt (%s)'),$username),
1015                $alert, false);
1016        }
1017    }
1018}
1019StaffAuthenticationBackend::register('StaffAuthStrikeBackend');
1020
1021/*
1022 * Backend to monitor user's failed login attempts
1023 */
1024class UserAuthStrikeBackend extends  AuthStrikeBackend {
1025
1026    function authTimeout() {
1027        global $ost;
1028
1029        $cfg = $ost->getConfig();
1030
1031        $authsession = &$_SESSION['_auth']['user'];
1032        if (!$authsession['laststrike'])
1033            return;
1034
1035        //Veto login due to excessive login attempts.
1036        if ((time()-$authsession['laststrike']) < $cfg->getStaffLoginTimeout()) {
1037            $authsession['laststrike'] = time(); //reset timer.
1038            return new AccessDenied(__("You've reached maximum failed login attempts allowed."));
1039        }
1040
1041        //Timeout is over.
1042        //Reset the counter for next round of attempts after the timeout.
1043        $authsession['laststrike']=null;
1044        $authsession['strikes']=0;
1045    }
1046
1047    function authstrike($credentials) {
1048        global $ost;
1049
1050        $cfg = $ost->getConfig();
1051
1052        $authsession = &$_SESSION['_auth']['user'];
1053
1054        $username = $credentials['username'];
1055        $password = $credentials['password'];
1056
1057        $authsession['strikes']+=1;
1058        if($authsession['strikes']>$cfg->getClientMaxLogins()) {
1059            $authsession['laststrike'] = time();
1060            $alert=_S('Excessive login attempts by a user.')."\n".
1061                    _S('Username').": {$username}\n".
1062                    _S('IP').": {$_SERVER['REMOTE_ADDR']}\n".
1063                    _S('Time').": ".date('M j, Y, g:i a T')."\n\n".
1064                    _S('Attempts').": {$authsession['strikes']}";
1065            $admin_alert = ($cfg->alertONLoginError() == 1 ? TRUE : FALSE);
1066            $ost->logError(_S('Excessive login attempts (user)'), $alert, $admin_alert);
1067
1068            if ($username) {
1069              $account = UserAccount::lookupByUsername($username);
1070              $id = UserEmailModel::getIdByEmail($username);
1071              if ($account)
1072                  $user = User::lookup($account->user_id);
1073              elseif ($id)
1074                $user = User::lookup($id);
1075
1076              if ($user) {
1077                $type = array('type' => 'login', 'msg' => sprintf('Excessive login attempts (%s)', $authsession['strikes']));
1078                Signal::send('person.login', $user, $type);
1079              }
1080            }
1081
1082            return new AccessDenied(__('Access denied'));
1083        } elseif($authsession['strikes']%3==0) { //Log every third failed login attempt as a warning.
1084            $alert=_S('Username').": {$username}\n".
1085                    _S('IP').": {$_SERVER['REMOTE_ADDR']}\n".
1086                    _S('Time').": ".date('M j, Y, g:i a T')."\n\n".
1087                    _S('Attempts').": {$authsession['strikes']}";
1088            $ost->logWarning(_S('Failed login attempt (user)'), $alert, false);
1089        }
1090
1091    }
1092}
1093UserAuthenticationBackend::register('UserAuthStrikeBackend');
1094
1095
1096class osTicketStaffAuthentication extends StaffAuthenticationBackend {
1097    static $name = "Local Authentication";
1098    static $id = "local";
1099
1100    function authenticate($username, $password) {
1101        if (($user = StaffSession::lookup($username)) && $user->getId() &&
1102                $user->check_passwd($password)) {
1103            try {
1104                $this->checkPolicies($user, $password);
1105            } catch (BadPassword | ExpiredPassword $ex) {
1106                $user->change_passwd = 1;
1107            }
1108            return $user;
1109        }
1110    }
1111
1112    function supportsPasswordChange() {
1113        return true;
1114    }
1115
1116    function syncPassword($staff, $password) {
1117        $staff->passwd = Passwd::hash($password);
1118    }
1119
1120    static function checkPassword($new, $current) {
1121        PasswordPolicy::checkPassword($new, $current, new self());
1122    }
1123
1124}
1125StaffAuthenticationBackend::register('osTicketStaffAuthentication');
1126
1127class PasswordResetTokenBackend extends StaffAuthenticationBackend {
1128    static $id = "pwreset.staff";
1129
1130    function supportsInteractiveAuthentication() {
1131        return false;
1132    }
1133
1134    function signOn($errors=array()) {
1135        global $ost;
1136
1137        if (!isset($_POST['userid']) || !isset($_POST['token']))
1138            return false;
1139        elseif (!($_config = new Config('pwreset')))
1140            return false;
1141
1142        $staff = StaffSession::lookup($_POST['userid']);
1143        if (!$staff || !$staff->getId())
1144            $errors['msg'] = __('Invalid user-id given');
1145        elseif (!($id = $_config->get($_POST['token']))
1146                || $id != $staff->getId())
1147            $errors['msg'] = __('Invalid reset token');
1148        elseif (!($ts = $_config->lastModified($_POST['token']))
1149                && ($ost->getConfig()->getPwResetWindow() < (time() - strtotime($ts))))
1150            $errors['msg'] = __('Invalid reset token');
1151        elseif (!$staff->forcePasswdRest())
1152            $errors['msg'] = __('Unable to reset password');
1153        else
1154            return $staff;
1155    }
1156
1157    function login($staff, $bk) {
1158        $_SESSION['_staff']['reset-token'] = $_POST['token'];
1159        Signal::send('auth.pwreset.login', $staff);
1160        return parent::login($staff, $bk);
1161    }
1162}
1163StaffAuthenticationBackend::register('PasswordResetTokenBackend');
1164
1165/*
1166 * AuthToken Authentication Backend
1167 *
1168 * Provides auto-login facility for end users with valid link
1169 *
1170 * Ticket used to loggin is tracked durring the session this is
1171 * important in the future when auto-logins will be
1172 * limited to single ticket view.
1173 */
1174class AuthTokenAuthentication extends UserAuthenticationBackend {
1175    static $name = "Auth Token Authentication";
1176    static $id = "authtoken";
1177
1178
1179    function signOn() {
1180        global $cfg;
1181
1182
1183        if (!$cfg || !$cfg->isAuthTokenEnabled())
1184            return null;
1185
1186        $user = null;
1187        if ($_GET['auth']) {
1188            if (($u = TicketUser::lookupByToken($_GET['auth'])))
1189                $user = new ClientSession($u);
1190        }
1191        // Support old ticket based tokens.
1192        elseif ($_GET['t'] && $_GET['e'] && $_GET['a']) {
1193            if (($ticket = Ticket::lookupByNumber($_GET['t'], $_GET['e']))
1194                    // Using old ticket auth code algo - hardcoded here because it
1195                    // will be removed in ticket class in the upcoming rewrite
1196                    && strcasecmp((string) $_GET['a'], md5($ticket->getId()
1197                            .  strtolower($_GET['e']) . SECRET_SALT)) === 0
1198                    && ($owner = $ticket->getOwner()))
1199                $user = new ClientSession($owner);
1200        }
1201
1202        return $user;
1203    }
1204
1205    function supportsInteractiveAuthentication() {
1206        return false;
1207    }
1208
1209    protected function getAuthKey($user) {
1210
1211        if (!$user)
1212            return null;
1213
1214        //Generate authkey based the type of ticket user
1215        // It's required to validate users going forward.
1216        $authkey = sprintf('%s%dt%dh%s',  //XXX: Placeholder
1217                    ($user->isOwner() ? 'o':'c'),
1218                    $user->getId(),
1219                    $user->getTicketId(),
1220                    md5($user->getId().$this->id));
1221
1222        return $authkey;
1223    }
1224
1225    protected function validate($authkey) {
1226
1227        $regex = '/^(?P<type>\w{1})(?P<id>\d+)t(?P<tid>\d+)h(?P<hash>.*)$/i';
1228        $matches = array();
1229        if (!preg_match($regex, $authkey, $matches))
1230            return false;
1231
1232        $user = null;
1233        switch ($matches['type']) {
1234            case 'c': //Collaborator
1235                $criteria = array(
1236                    'user_id' => $matches['id'],
1237                    'thread__ticket__ticket_id' => $matches['tid']
1238                );
1239                if (($c = Collaborator::lookup($criteria))
1240                        && ($c->getTicketId() == $matches['tid']))
1241                    $user = new ClientSession($c);
1242                break;
1243            case 'o': //Ticket owner
1244                if (($ticket = Ticket::lookup($matches['tid']))
1245                        && ($o = $ticket->getOwner())
1246                        && ($o->getId() == $matches['id']))
1247                    $user = new ClientSession($o);
1248                break;
1249        }
1250
1251        //Make sure the authkey matches.
1252        if (!$user || strcmp($this->getAuthKey($user), $authkey))
1253            return null;
1254
1255        $user->flagGuest();
1256
1257        return $user;
1258    }
1259
1260}
1261
1262UserAuthenticationBackend::register('AuthTokenAuthentication');
1263
1264//Simple ticket lookup backend used to recover ticket access link.
1265// We're using authentication backend so we can guard aganist brute force
1266// attempts (which doesn't buy much since the link is emailed)
1267class AccessLinkAuthentication extends UserAuthenticationBackend {
1268    static $name = "Ticket Access Link Authentication";
1269    static $id = "authlink";
1270
1271    function authenticate($email, $number) {
1272
1273        if (!($ticket = Ticket::lookupByNumber($number))
1274                || !($user=User::lookup(array('emails__address' => $email))))
1275            return false;
1276
1277        if (!($user = $this->_getTicketUser($ticket, $user)))
1278            return false;
1279
1280        $_SESSION['_auth']['user-ticket'] = $number;
1281        return new ClientSession($user);
1282    }
1283
1284    function _getTicketUser($ticket, $user) {
1285        // Ticket owner?
1286        if ($ticket->getUserId() == $user->getId())
1287            $user = $ticket->getOwner();
1288        // Collaborator?
1289        elseif (!($user = Collaborator::lookup(array(
1290                'user_id' => $user->getId(),
1291                'thread__ticket__ticket_id' => $ticket->getId())
1292        )))
1293            return false; //Bro, we don't know you!
1294
1295        return $user;
1296    }
1297
1298    // We are not actually logging in the user....
1299    function login($user, $bk) {
1300        global $cfg;
1301
1302        if (!$cfg->isClientEmailVerificationRequired()) {
1303            return parent::login($user, $bk);
1304        }
1305        return true;
1306    }
1307
1308    protected function validate($userid) {
1309        $number = $_SESSION['_auth']['user-ticket'];
1310
1311        if (!($ticket = Ticket::lookupByNumber($number)))
1312            return false;
1313
1314        if (!($user = User::lookup($userid)))
1315            return false;
1316
1317        if (!($user = $this->_getTicketUser($ticket, $user)))
1318            return false;
1319
1320        $user = new ClientSession($user);
1321        $user->flagGuest();
1322        return $user;
1323    }
1324
1325    function supportsInteractiveAuthentication() {
1326        return false;
1327    }
1328}
1329UserAuthenticationBackend::register('AccessLinkAuthentication');
1330
1331class osTicketClientAuthentication extends UserAuthenticationBackend {
1332    static $name = "Local Client Authentication";
1333    static $id = "client";
1334
1335    function authenticate($username, $password) {
1336        if (!($acct = ClientAccount::lookupByUsername($username)))
1337            return;
1338
1339        if (($client = new ClientSession(new EndUser($acct->getUser())))
1340                && !$client->getId())
1341            return false;
1342        elseif (!$acct->check_passwd($password))
1343            return false;
1344        else
1345            return $client;
1346    }
1347
1348    static function checkPassword($new, $current) {
1349        PasswordPolicy::checkPassword($new, $current, new self());
1350    }
1351}
1352UserAuthenticationBackend::register('osTicketClientAuthentication');
1353
1354class ClientPasswordResetTokenBackend extends UserAuthenticationBackend {
1355    static $id = "pwreset.client";
1356
1357    function supportsInteractiveAuthentication() {
1358        return false;
1359    }
1360
1361    function signOn($errors=array()) {
1362        global $ost;
1363
1364        if (!isset($_POST['userid']) || !isset($_POST['token']))
1365            return false;
1366        elseif (!($_config = new Config('pwreset')))
1367            return false;
1368        elseif (!($acct = ClientAccount::lookupByUsername($_POST['userid']))
1369                || !$acct->getId()
1370                || !($client = new ClientSession(new EndUser($acct->getUser()))))
1371            $errors['msg'] = __('Invalid user-id given');
1372        elseif (!($id = $_config->get($_POST['token']))
1373                || $id != 'c'.$client->getId())
1374            $errors['msg'] = __('Invalid reset token');
1375        elseif (!($ts = $_config->lastModified($_POST['token']))
1376                && ($ost->getConfig()->getPwResetWindow() < (time() - strtotime($ts))))
1377            $errors['msg'] = __('Invalid reset token');
1378        elseif (!$acct->forcePasswdReset())
1379            $errors['msg'] = __('Unable to reset password');
1380        else
1381            return $client;
1382    }
1383
1384    function login($client, $bk) {
1385        $_SESSION['_client']['reset-token'] = $_POST['token'];
1386        Signal::send('auth.pwreset.login', $client);
1387        return parent::login($client, $bk);
1388    }
1389}
1390UserAuthenticationBackend::register('ClientPasswordResetTokenBackend');
1391
1392class ClientAcctConfirmationTokenBackend extends UserAuthenticationBackend {
1393    static $id = "confirm.client";
1394
1395    function supportsInteractiveAuthentication() {
1396        return false;
1397    }
1398
1399    function signOn($errors=array()) {
1400        global $ost;
1401
1402        if (!isset($_GET['token']))
1403            return false;
1404        elseif (!($_config = new Config('pwreset')))
1405            return false;
1406        elseif (!($id = $_config->get($_GET['token'])))
1407            return false;
1408        elseif (!($acct = ClientAccount::lookup(array('user_id'=>substr($id,1))))
1409                || !$acct->getId()
1410                || $id != 'c'.$acct->getUserId()
1411                || !($client = new ClientSession(new EndUser($acct->getUser()))))
1412            return false;
1413        else
1414            return $client;
1415    }
1416}
1417UserAuthenticationBackend::register('ClientAcctConfirmationTokenBackend');
1418
1419// ----- Password Policy --------------------------------------
1420
1421class BadPassword extends Exception {}
1422class ExpiredPassword extends Exception {}
1423class PasswordUpdateFailed extends Exception {}
1424
1425abstract class PasswordPolicy {
1426    static protected $registry = array();
1427
1428    static $id;
1429    static $name;
1430
1431    /**
1432     * Check a password and throw BadPassword with a meaningful message if
1433     * the password cannot be accepted.
1434     */
1435    abstract function onset($new, $current);
1436
1437    /*
1438     * Called on login to enforce policies & check for expired passwords
1439     */
1440    abstract function onLogin($user, $password);
1441
1442    /*
1443     * get friendly name of the policy
1444     */
1445    function getName() {
1446        return static::$name;
1447    }
1448
1449    /*
1450     * Check a password aganist all available policies
1451     */
1452    static function checkPassword($new, $current, $bk=null) {
1453        if ($bk && is_a($bk, 'AuthenticationBackend'))
1454            $policies = $bk->getPasswordPolicies();
1455        else
1456            $policies = self::allActivePolicies();
1457
1458        foreach ($policies as $P)
1459            $P->onSet($new, $current);
1460    }
1461
1462    static function allActivePolicies() {
1463        $policies = array();
1464        foreach (array_reverse(static::$registry) as $P) {
1465            if (is_string($P) && class_exists($P))
1466                $P = new $P();
1467            if ($P instanceof PasswordPolicy)
1468                $policies[] = $P;
1469        }
1470        return $policies;
1471    }
1472
1473    static function register($policy) {
1474        static::$registry[] = $policy;
1475    }
1476
1477    static function cleanSessions($model, $user=null) {
1478        $criteria = array();
1479
1480        switch (true) {
1481            case ($model instanceof Staff):
1482                $criteria['user_id'] = $model->getId();
1483
1484                if ($user && ($model->getId() == $user->getId()))
1485                    array_push($criteria,
1486                        Q::not(array('session_id' => $user->session->session_id)));
1487                break;
1488            case ($model instanceof User):
1489                $regexp = '_auth\|.*"user";[a-z]+:[0-9]+:\{[a-z]+:[0-9]+:"id";[a-z]+:'.$model->getId();
1490                $criteria['user_id'] = 0;
1491                $criteria['session_data__regex'] = $regexp;
1492
1493                if ($user)
1494                    array_push($criteria,
1495                        Q::not(array('session_id' => $user->session->session_id)));
1496                break;
1497            default:
1498                return false;
1499        }
1500
1501        return SessionData::objects()->filter($criteria)->delete();
1502    }
1503}
1504Signal::connect('auth.clean', array('PasswordPolicy', 'cleanSessions'));
1505
1506/*
1507 * Basic default password policy that ships with osTicket.
1508 *
1509 */
1510class osTicketPasswordPolicy
1511extends PasswordPolicy {
1512    static $id = "basic";
1513    static $name = /* @trans */ "Default Basic Policy";
1514
1515    function onLogin($user, $password) {
1516        global $cfg;
1517
1518        // Check for possible password expiration
1519        // Check is only here for legacy reasons - password management
1520        // policies are now done via plugins.
1521        if ($cfg && $user
1522                && ($period=$cfg->getPasswdResetPeriod())
1523                && ($time=$user->getPasswdResetTimestamp())
1524                && $time < time()-($period*2629800))
1525            throw new ExpiredPassword(__('Expired password'));
1526    }
1527
1528    function onSet($passwd, $current) {
1529        if (strlen($passwd) < 6) {
1530            throw new BadPassword(
1531                __('Password must be at least 6 characters'));
1532        }
1533        // XXX: Changing case is technicall changing the password
1534        if (0 === strcasecmp($passwd, $current)) {
1535            throw new BadPassword(
1536                __('New password MUST be different from the current password!'));
1537        }
1538    }
1539}
1540PasswordPolicy::register('osTicketPasswordPolicy');
1541?>
1542