diff --git a/assets/default/css/theme.css b/assets/default/css/theme.css
index c4bd447e8ce924d267453b60b4b0faa764ce5a5f..e7056b813e47b2d0e95548fd77f1e35ac4b352cc 100644
--- a/assets/default/css/theme.css
+++ b/assets/default/css/theme.css
@@ -634,7 +634,7 @@ label.required {
 }
 #clientLogin p {
   clear: both;
-  text-align: center;
+  text-align: right;
 }
 #clientLogin strong {
   font-size: 11px;
diff --git a/bootstrap.php b/bootstrap.php
index 0a50c4a2635b693e575d87a3a13dda9f66e3a997..c3a02a92f41681f1c489443aaa39b4a23a2d8833 100644
--- a/bootstrap.php
+++ b/bootstrap.php
@@ -70,6 +70,7 @@ class Bootstrap {
         define('ATTACHMENT_TABLE',$prefix.'attachment');
         define('USER_TABLE',$prefix.'user');
         define('USER_EMAIL_TABLE',$prefix.'user_email');
+        define('USER_ACCOUNT_TABLE',$prefix.'user_account');
 
         define('STAFF_TABLE',$prefix.'staff');
         define('TEAM_TABLE',$prefix.'team');
diff --git a/include/class.auth.php b/include/class.auth.php
index 660acf9ba2aba5946acd40fd2ec07bedea154237..9e981d0119df2378d00b6fe27f302ba4318697d4 100644
--- a/include/class.auth.php
+++ b/include/class.auth.php
@@ -797,38 +797,41 @@ class AuthTokenAuthentication extends UserAuthenticationBackend {
 }
 UserAuthenticationBackend::register('AuthTokenAuthentication');
 
-//Simple ticket lookup backend used to recover ticket access link.
-// We're using authentication backend so we can guard aganist brute force
-// attempts (which doesn't buy much since the link is emailed)
-class AccessLinkAuthentication extends UserAuthenticationBackend {
-    static $name = "Ticket Access Link Authentication";
-    static $id = "authlink";
-
-    function authenticate($email, $number) {
-
-        if (!($ticket = Ticket::lookupByNumber($number))
-                || !($user=User::lookup(array('emails__address' =>
-                            $email))))
-            return false;
+class osTicketClientAuthentication extends UserAuthenticationBackend {
+    static $name = "Local Client Authentication";
+    static $id = "client";
 
-        //Ticket owner?
-        if ($ticket->getUserId() == $user->getId())
-            $user = $ticket->getOwner();
-        //Collaborator?
-        elseif (!($user = Collaborator::lookup(array('userId' =>
-                            $user->getId(), 'ticketId' =>
-                            $ticket->getId()))))
-            return false; //Bro, we don't know you!
+    function authenticate($username, $password) {
+        if (strpos($username, '@') !== false)
+            $user = User::lookup(array('emails__address'=>$username));
+        else
+            $user = User::lookup(array('account__username'=>$username));
 
+        if (!$user)
+            return;
 
-        return new ClientSession($user);
+        if (($client = new ClientSession(new EndUser($user)))
+            && $client->getId()
+            && ($acct = $client->getAccount())
+            && $acct->checkPassword($password)
+        ) {
+            return $client;
+        }
     }
 
-    //We are not actually logging in the user....
-    function login($user, $bk) {
-        return true;
+    protected function validate($authkey) {
+        if (strpos($authkey, '@') !== false)
+            $user = User::lookup(array('emails__address'=>$authkey));
+        else
+            $user = User::lookup(array('account__authkey'=>$authkey));
+
+        if (!$user)
+            return;
+
+        if (($client = new ClientSession(new EndUser($user))) && $client->getId())
+            return $client;
     }
 
 }
-UserAuthenticationBackend::register('AccessLinkAuthentication');
+UserAuthenticationBackend::register('osTicketClientAuthentication');
 ?>
diff --git a/include/class.client.php b/include/class.client.php
index c9f6fc394ec8fa53619d2d1b2867dfdccbb3db3d..5af39ac61b5f4a2c9b6ecead082fb31ee7f469bd 100644
--- a/include/class.client.php
+++ b/include/class.client.php
@@ -187,7 +187,10 @@ class  EndUser extends AuthenticatedUser {
         if ($this->user instanceof Collaborator)
             return $this->user->getUserId();
 
-        return $this->user->getId();
+        elseif ($this->user)
+            return $this->user->getId();
+
+        return false;
     }
 
     function getUserName() {
@@ -225,6 +228,13 @@ class  EndUser extends AuthenticatedUser {
         return ($stats=$this->getTicketStats())?$stats['closed']:0;
     }
 
+    function hasAccount() {
+    }
+
+    function getAccount() {
+        return ClientAccount::lookup(array('user_id'=>$this->getId()));
+    }
+
     private function getStats() {
 
         $sql='SELECT count(open.ticket_id) as open, count(closed.ticket_id) as closed '
@@ -242,4 +252,51 @@ class  EndUser extends AuthenticatedUser {
         return db_fetch_array(db_query($sql));
     }
 }
+
+require_once INCLUDE_DIR.'class.orm.php';
+class ClientAccountModel extends VerySimpleModel {
+    static $meta = array(
+        'table' => USER_ACCOUNT_TABLE,
+        'pk' => 'id',
+        'joins' => array(
+            'user' => array(
+                'null' => false,
+                'constraint' => array('user_id' => 'UserModel.id')
+            ),
+        ),
+    );
+}
+
+class ClientAccount extends ClientAccountModel {
+    var $_options = null;
+
+    const LOCKED = 0x0001;
+    const PASSWD_RESET_REQUIRED = 0x0002;
+
+    function checkPassword($password, $autoupdate=true) {
+
+        /*bcrypt based password match*/
+        if(Passwd::cmp($password, $this->get('passwd')))
+            return true;
+
+        //Fall back to MD5
+        if(!$password || strcmp($this->get('passwd'), MD5($password)))
+            return false;
+
+        //Password is a MD5 hash: rehash it (if enabled) otherwise force passwd change.
+        if ($autoupdate)
+            $this->set('passwd', Passwd::hash($password));
+
+        if (!$autoupdate || !$this->save())
+            $this->forcePasswdReset();
+
+        return true;
+    }
+
+    function forcePasswdReset() {
+        $this->set('status', $this->get('status') | self::PASSWD_RESET_REQUIRED);
+        $this->save();
+    }
+}
+
 ?>
diff --git a/include/class.orm.php b/include/class.orm.php
index b9bda6a1176ac4beea1aee9301360e50328a315b..951a4ffd425e88109f35c103aeed6e425bd2fb52 100644
--- a/include/class.orm.php
+++ b/include/class.orm.php
@@ -113,7 +113,8 @@ class VerySimpleModel {
                     $constraint[$field] = "$model.$foreign";
                 }
                 $j['constraint'] = $constraint;
-                $j['list'] = true;
+                if (!isset($j['list']))
+                    $j['list'] = true;
             }
             // XXX: Make this better (ie. composite keys)
             $keys = array_keys($j['constraint']);
diff --git a/include/class.user.php b/include/class.user.php
index 0a945f68b57c1b37ed93c59fa40fbd7b5ba45969..c5b0f5b9e47b67bbf8027cd8af15bc583ea4c7da 100644
--- a/include/class.user.php
+++ b/include/class.user.php
@@ -36,6 +36,10 @@ class UserModel extends VerySimpleModel {
             'emails' => array(
                 'reverse' => 'UserEmailModel.user',
             ),
+            'account' => array(
+                'list' => false,
+                'reverse' => 'ClientAccountModel.user',
+            ),
             'default_email' => array(
                 'null' => true,
                 'constraint' => array('default_email_id' => 'UserEmailModel.id')
diff --git a/include/client/login.inc.php b/include/client/login.inc.php
index 586ef9808b75144e37dafc775debb9cc8f4ca1c4..9ed334e951094f02cb173fec38fa7106b1e2a728 100644
--- a/include/client/login.inc.php
+++ b/include/client/login.inc.php
@@ -1,26 +1,24 @@
 <?php
 if(!defined('OSTCLIENTINC')) die('Access Denied');
 
-$email=Format::input($_POST['lemail']?$_POST['lemail']:$_GET['e']);
-$ticketid=Format::input($_POST['lticket']?$_POST['lticket']:$_GET['t']);
+$email=Format::input($_POST['luser']?:$_GET['e']);
+$passwd=Format::input($_POST['lpasswd']?:$_GET['t']);
 ?>
-<h1>Check Ticket Status</h1>
-<p>Please provide us with your email address and a ticket number, and an access
-link will be emailed to you.</p>
+<h1>Sign In</h1>
 <form action="login.php" method="post" id="clientLogin">
     <?php csrf_token(); ?>
     <strong><?php echo Format::htmlchars($errors['login']); ?></strong>
     <br>
     <div>
-        <label for="email">E-Mail Address:</label>
-        <input id="email" type="text" name="lemail" size="30" value="<?php echo $email; ?>">
+        <label for="username">Username:</label>
+        <input id="username" type="text" name="luser" size="30" value="<?php echo $email; ?>">
     </div>
     <div>
-        <label for="ticketno">Ticket Number:</label>
-        <input id="ticketno" type="text" name="lticket" size="16" value="<?php echo $ticketid; ?>"></td>
+        <label for="passwd">Password:</label>
+        <input id="passwd" type="password" name="lpasswd" size="30" value="<?php echo $passwd; ?>"></td>
     </div>
     <p>
-        <input class="btn" type="submit" value="Email Access Link">
+        <input class="btn" type="submit" value="Sign In">
     </p>
 </form>
 <br>
diff --git a/login.php b/login.php
index 9e6d5f36f18d64384f96818ae82498a774553344..4751f74976b550827221ebcf7292b1d5b72bda24 100644
--- a/login.php
+++ b/login.php
@@ -26,15 +26,11 @@ require_once(INCLUDE_DIR.'class.ticket.php');
 
 $inc = 'login.inc.php';
 if ($_POST) {
-    if (!$_POST['lticket'] || !Validator::is_email($_POST['lemail']))
-        $errors['err'] = 'Valid email address and ticket number required';
-    elseif (($user = UserAuthenticationBackend::process($_POST['lemail'],
-                    $_POST['lticket'], $errors))) {
-        //We're using authentication backend so we can guard aganist brute
-        // force attempts (which doesn't buy much since the link is emailed)
-        $user->sendAccessLink();
-        $msg = sprintf("%s - access link sent to your email!",
-            $user->getName()->getFirst());
+    if (!$_POST['luser'])
+        $errors['err'] = 'Valid username or email address is required';
+    elseif (($user = UserAuthenticationBackend::process($_POST['luser'],
+            $_POST['lpasswd'], $errors))) {
+        Http::redirect('tickets.php');
         $_POST = null;
     } elseif(!$errors['err']) {
         $errors['err'] = 'Invalid email or ticket number - try again!';