move session init into utils, re-fetch session after login
[authserver.git] / index.php
index 841333bb3433ab70ab766878341819cd3d1d0cd5..c69424b7e8f6327240a0ff6144ba896be6a098ad 100644 (file)
--- a/index.php
+++ b/index.php
@@ -6,8 +6,6 @@
 // Include the common auth system files (including the OAuth2 Server object).
 require_once(__DIR__.'/authsystem.inc.php');
 
-$errors = array();
-
 // Start HTML document as a DOM object.
 extract(ExtendedDocument::initHTML5()); // sets $document, $html, $head, $title, $body
 $document->formatOutput = true; // we want a nice output
@@ -19,314 +17,297 @@ $head->appendJSFile('authsystem.js');
 $title->appendText('KaiRo.at Authentication Server');
 $h1 = $body->appendElement('h1', 'KaiRo.at Authentication Server');
 
-$errors += $utils->checkForSecureConnection();
+$errors = $utils->checkForSecureConnection();
 
 $para = $body->appendElement('p', _('This login system does not work without JavaScript. Please activate JavaScript for this site to log in.'));
 $para->setAttribute('id', 'jswarning');
 $para->setAttribute('class', 'warn');
 
 if (!count($errors)) {
-  $session = null;
+  $session = $utils->initSession(); // Read session or create new session and set cookie.
   $user = array('id' => 0, 'email' => '');
   $pagetype = 'default';
-  $db->exec("SET time_zone='+00:00';"); // Execute directly on PDO object, set session to UTC to make our gmdate() values match correctly.
-  if (strlen(@$_COOKIE['sessionkey'])) {
-    // Fetch the session - or at least try to.
-    $result = $db->prepare('SELECT * FROM `auth_sessions` WHERE `sesskey` = :sesskey AND `time_expire` > :expire;');
-    $result->execute(array(':sesskey' => $_COOKIE['sessionkey'], ':expire' => gmdate('Y-m-d H:i:s')));
-    $row = $result->fetch(PDO::FETCH_ASSOC);
-    if ($row) {
-      $session = $row;
-
-      if (array_key_exists('logout', $_GET)) {
-        $result = $db->prepare('UPDATE `auth_sessions` SET `logged_in` = FALSE WHERE `id` = :sessid;');
-        if (!$result->execute(array(':sessid' => $session['id']))) {
-          $utils->log('logout_failure', 'session: '.$session['id']);
-          $errors[] = _('The email address is invalid.');
-        }
-        $session['logged_in'] = 0;
-      }
-      elseif (array_key_exists('email', $_POST)) {
-        if (!preg_match('/^[^@]+@[^@]+\.[^@]+$/', $_POST['email'])) {
-          $errors[] = _('The email address is invalid.');
-        }
-        elseif ($utils->verifyTimeCode(@$_POST['tcode'], $session)) {
-          $result = $db->prepare('SELECT `id`, `pwdhash`, `email`, `status`, `verify_hash` FROM `auth_users` WHERE `email` = :email;');
-          $result->execute(array(':email' => $_POST['email']));
-          $user = $result->fetch(PDO::FETCH_ASSOC);
-          if ($user['id'] && array_key_exists('pwd', $_POST)) {
-            // existing user, check password
-            if (($user['status'] == 'ok') && $utils->pwdVerify(@$_POST['pwd'], $user)) {
-              // Check if a newer hashing algorithm is available
-              // or the cost has changed
-              if ($utils->pwdNeedsRehash($user)) {
-                // If so, create a new hash, and replace the old one
-                $newHash = $utils->pwdHash($_POST['pwd']);
-                $result = $db->prepare('UPDATE `auth_users` SET `pwdhash` = :pwdhash WHERE `id` = :userid;');
-                if (!$result->execute(array(':pwdhash' => $newHash, ':userid' => $user['id']))) {
-                  $utils->log('user_hash_save_failure', 'user: '.$user['id']);
-                }
-                else {
-                  $utils->log('pwd_rehash_success', 'user: '.$user['id']);
-                }
-              }
+  if (is_null($session)) {
+    $errors[] = _('The session system is not working. Please <a href="https://www.kairo.at/contact">contact KaiRo.at</a> and tell the team about this.');
+  }
+  elseif (array_key_exists('logout', $_GET)) {
+    $result = $db->prepare('UPDATE `auth_sessions` SET `logged_in` = FALSE WHERE `id` = :sessid;');
+    if (!$result->execute(array(':sessid' => $session['id']))) {
+      $utils->log('logout_failure', 'session: '.$session['id']);
+      $errors[] = _('The email address is invalid.');
+    }
+    $session['logged_in'] = 0;
+  }
+  elseif (array_key_exists('email', $_POST)) {
+    if (!preg_match('/^[^@]+@[^@]+\.[^@]+$/', $_POST['email'])) {
+      $errors[] = _('The email address is invalid.');
+    }
+    elseif ($utils->verifyTimeCode(@$_POST['tcode'], $session)) {
+      $result = $db->prepare('SELECT `id`, `pwdhash`, `email`, `status`, `verify_hash` FROM `auth_users` WHERE `email` = :email;');
+      $result->execute(array(':email' => $_POST['email']));
+      $user = $result->fetch(PDO::FETCH_ASSOC);
+      if ($user['id'] && array_key_exists('pwd', $_POST)) {
+        // existing user, check password
+        if (($user['status'] == 'ok') && $utils->pwdVerify(@$_POST['pwd'], $user)) {
+          // Check if a newer hashing algorithm is available
+          // or the cost has changed
+          if ($utils->pwdNeedsRehash($user)) {
+            // If so, create a new hash, and replace the old one
+            $newHash = $utils->pwdHash($_POST['pwd']);
+            $result = $db->prepare('UPDATE `auth_users` SET `pwdhash` = :pwdhash WHERE `id` = :userid;');
+            if (!$result->execute(array(':pwdhash' => $newHash, ':userid' => $user['id']))) {
+              $utils->log('user_hash_save_failure', 'user: '.$user['id']);
+            }
+            else {
+              $utils->log('pwd_rehash_success', 'user: '.$user['id']);
+            }
+          }
 
-              // Log user in - update session key for that, see https://wiki.mozilla.org/WebAppSec/Secure_Coding_Guidelines#Login
-              $utils->log('login', 'user: '.$user['id']);
-              $sesskey = $utils->createSessionKey();
-              setcookie('sessionkey', $sesskey, 0, "", "", !$utils->running_on_localhost, true); // Last two params are secure and httponly, secure is not set on localhost.
-              // If the session has a user set, create a new one - otherwise take existing session entry.
-              if (intval($session['user'])) {
-                $result = $db->prepare('INSERT INTO `auth_sessions` (`sesskey`, `time_expire`, `user`, `logged_in`) VALUES (:sesskey, :expire, :userid, TRUE);');
-                $result->execute(array(':sesskey' => $sesskey, ':userid' => $user['id'], ':expire' => gmdate('Y-m-d H:i:s', strtotime('+1 day'))));
-                // After insert, actually fetch the session row from the DB so we have all values.
-                $result = $db->prepare('SELECT * FROM auth_sessions WHERE `sesskey` = :sesskey AND `time_expire` > :expire;');
-                $result->execute(array(':sesskey' => $sesskey, ':expire' => gmdate('Y-m-d H:i:s')));
-                $row = $result->fetch(PDO::FETCH_ASSOC);
-                if ($row) {
-                  $session = $row;
-                }
-                else {
-                  $utils->log('create_session_failure', 'at login, prev session: '.$session['id'].', new user: '.$user['id']);
-                  $errors[] = _('The session system is not working. Please <a href="https://www.kairo.at/contact">contact KaiRo.at</a> and tell the team about this.');
-                }
-              }
-              else {
-                $result = $db->prepare('UPDATE `auth_sessions` SET `sesskey` = :sesskey, `user` = :userid, `logged_in` = TRUE, `time_expire` = :expire WHERE `id` = :sessid;');
-                if (!$result->execute(array(':sesskey' => $sesskey, ':userid' => $user['id'], ':expire' => gmdate('Y-m-d H:i:s', strtotime('+1 day')), ':sessid' => $session['id']))) {
-                  $utils->log('login_failure', 'session: '.$session['id'].', user: '.$user['id']);
-                  $errors[] = _('Login failed unexpectedly. Please <a href="https://www.kairo.at/contact">contact KaiRo.at</a> and tell the team about this.');
-                }
-              }
-              // If a verify_hash if set on a verified user, a password reset had been requested. As a login works right now, cancel that reset request by deleting the hash.
-              if (strlen(@$user['verify_hash'])) {
-                $result = $db->prepare('UPDATE `auth_users` SET `verify_hash` = \'\' WHERE `id` = :userid;');
-                if (!$result->execute(array(':userid' => $user['id']))) {
-                  $utils->log('empty_vhash_failure', 'user: '.$user['id']);
-                }
-                else {
-                  $user['verify_hash'] = '';
-                }
-              }
+          // Log user in - update session key for that, see https://wiki.mozilla.org/WebAppSec/Secure_Coding_Guidelines#Login
+          $utils->log('login', 'user: '.$user['id']);
+          $sesskey = $utils->createSessionKey();
+          setcookie('sessionkey', $sesskey, 0, "", "", !$utils->running_on_localhost, true); // Last two params are secure and httponly, secure is not set on localhost.
+          // If the session has a user set, create a new one - otherwise take existing session entry.
+          if (intval($session['user'])) {
+            $result = $db->prepare('INSERT INTO `auth_sessions` (`sesskey`, `time_expire`, `user`, `logged_in`) VALUES (:sesskey, :expire, :userid, TRUE);');
+            $result->execute(array(':sesskey' => $sesskey, ':userid' => $user['id'], ':expire' => gmdate('Y-m-d H:i:s', strtotime('+1 day'))));
+            // After insert, actually fetch the session row from the DB so we have all values.
+            $result = $db->prepare('SELECT * FROM auth_sessions WHERE `sesskey` = :sesskey AND `time_expire` > :expire;');
+            $result->execute(array(':sesskey' => $sesskey, ':expire' => gmdate('Y-m-d H:i:s')));
+            $row = $result->fetch(PDO::FETCH_ASSOC);
+            if ($row) {
+              $session = $row;
             }
             else {
-              $errors[] = _('This password is invalid or your email is not verified yet. Did you type them correctly?');
+              $utils->log('create_session_failure', 'at login, prev session: '.$session['id'].', new user: '.$user['id']);
+              $errors[] = _('The session system is not working. Please <a href="https://www.kairo.at/contact">contact KaiRo.at</a> and tell the team about this.');
             }
           }
           else {
-            // new user: check password, create user and send verification; existing users: re-send verification or send password change instructions
-            if (array_key_exists('pwd', $_POST)) {
-              $errors += $utils->checkPasswordConstraints(strval($_POST['pwd']), $_POST['email']);
+            $result = $db->prepare('UPDATE `auth_sessions` SET `sesskey` = :sesskey, `user` = :userid, `logged_in` = TRUE, `time_expire` = :expire WHERE `id` = :sessid;');
+            if (!$result->execute(array(':sesskey' => $sesskey, ':userid' => $user['id'], ':expire' => gmdate('Y-m-d H:i:s', strtotime('+1 day')), ':sessid' => $session['id']))) {
+              $utils->log('login_failure', 'session: '.$session['id'].', user: '.$user['id']);
+              $errors[] = _('Login failed unexpectedly. Please <a href="https://www.kairo.at/contact">contact KaiRo.at</a> and tell the team about this.');
             }
-            if (!count($errors)) {
-              // Put user into the DB
-              if (!$user['id']) {
-                $newHash = $utils->pwdHash($_POST['pwd']);
-                $vcode = $utils->createVerificationCode();
-                $result = $db->prepare('INSERT INTO `auth_users` (`email`, `pwdhash`, `status`, `verify_hash`) VALUES (:email, :pwdhash, \'unverified\', :vcode);');
-                if (!$result->execute(array(':email' => $_POST['email'], ':pwdhash' => $newHash, ':vcode' => $vcode))) {
-                  $utils->log('user_insert_failure', 'email: '.$_POST['email']);
-                  $errors[] = _('Could not add user. Please <a href="https://www.kairo.at/contact">contact KaiRo.at</a> and tell the team about this.');
-                }
-                $user = array('id' => $db->lastInsertId(),
-                              'email' => $_POST['email'],
-                              'pwdhash' => $newHash,
-                              'status' => 'unverified',
-                              'verify_hash' => $vcode);
-                $utils->log('new_user', 'user: '.$user['id'].', email: '.$user['email']);
-              }
-              if ($user['status'] == 'unverified') {
-                // Send email for verification and show message to point to it.
-                $mail = new email();
-                $mail->setCharset('utf-8');
-                $mail->addHeader('X-KAIRO-AUTH', 'email_verification');
-                $mail->addRecipient($user['email']);
-                $mail->setSender('noreply@auth.kairo.at', _('KaiRo.at Authentication Service'));
-                $mail->setSubject('Email Verification for KaiRo.at Authentication');
-                $mail->addMailText(_('Welcome!')."\n\n");
-                $mail->addMailText(sprintf(_('This email address, %s, has been used for registration on "%s".'),
-                                          $user['email'], _('KaiRo.at Authentication Service'))."\n\n");
-                $mail->addMailText(_('Please confirm that registration by clicking the following link (or calling it up in your browser):')."\n");
-                $mail->addMailText(($utils->running_on_localhost?'http':'https').'://'.$_SERVER['SERVER_NAME'].strstr($_SERVER['REQUEST_URI'], '?', true)
-                                  .'?email='.rawurlencode($user['email']).'&verification_code='.rawurlencode($user['verify_hash'])."\n\n");
-                $mail->addMailText(_('With this confirmation, you accept that we handle your data for the purpose of logging you into other websites when you request that.')."\n");
-                $mail->addMailText(_('Those websites will get to know your email address but not your password, which we store securely.')."\n");
-                $mail->addMailText(_('If you do not call this confirmation link within 72 hours, your data will be deleted from our database.')."\n\n");
-                $mail->addMailText(sprintf(_('The %s team'), 'KaiRo.at'));
-                //$mail->setDebugAddress("robert@localhost");
-                $mailsent = $mail->send();
-                if ($mailsent) {
-                  $pagetype = 'verification_sent';
-                }
-                else {
-                  $utils->log('verify_mail_failure', 'user: '.$user['id'].', email: '.$user['email']);
-                  $errors[] = _('The confirmation email could not be sent to you. Please <a href="https://www.kairo.at/contact">contact KaiRo.at</a> and tell the team about this.');
-                }
-              }
-              else {
-                // Password reset requested with "Password forgotten?" function.
-                $vcode = $utils->createVerificationCode();
-                $result = $db->prepare('UPDATE `auth_users` SET `verify_hash` = :vcode WHERE `id` = :userid;');
-                if (!$result->execute(array(':vcode' => $vcode, ':userid' => $user['id']))) {
-                  $utils->log('vhash_set_failure', 'user: '.$user['id']);
-                  $errors[] = _('Could not initiate reset request. Please <a href="https://www.kairo.at/contact">contact KaiRo.at</a> and tell the team about this.');
-                }
-                else {
-                  $utils->log('pwd_reset_request', 'user: '.$user['id'].', email: '.$user['email']);
-                  $resetcode = $vcode.dechex($user['id'] + $session['id']).'_'.$utils->createTimeCode($session, null, 60);
-                  // Send email with instructions for resetting the password.
-                  $mail = new email();
-                  $mail->setCharset('utf-8');
-                  $mail->addHeader('X-KAIRO-AUTH', 'password_reset');
-                  $mail->addRecipient($user['email']);
-                  $mail->setSender('noreply@auth.kairo.at', _('KaiRo.at Authentication Service'));
-                  $mail->setSubject('How to reset your password for KaiRo.at Authentication');
-                  $mail->addMailText(_('Hi,')."\n\n");
-                  $mail->addMailText(sprintf(_('A request for setting a new password for this email address, %s, has been submitted on "%s".'),
-                                            $user['email'], _('KaiRo.at Authentication Service'))."\n\n");
-                  $mail->addMailText(_('You can set a new password by clicking the following link (or calling it up in your browser):')."\n");
-                  $mail->addMailText(($utils->running_on_localhost?'http':'https').'://'.$_SERVER['SERVER_NAME'].strstr($_SERVER['REQUEST_URI'], '?', true)
-                                    .'?email='.rawurlencode($user['email']).'&reset_code='.rawurlencode($resetcode)."\n\n");
-                  $mail->addMailText(_('If you do not call this confirmation link within 1 hour, this link expires and the existing password is being kept in place.')."\n\n");
-                  $mail->addMailText(sprintf(_('The %s team'), 'KaiRo.at'));
-                  //$mail->setDebugAddress("robert@localhost");
-                  $mailsent = $mail->send();
-                  if ($mailsent) {
-                    $pagetype = 'resetmail_sent';
-                  }
-                  else {
-                    $utils->log('pwd_reset_mail_failure', 'user: '.$user['id'].', email: '.$user['email']);
-                    $errors[] = _('The email with password reset instructions could not be sent to you. Please <a href="https://www.kairo.at/contact">contact KaiRo.at</a> and tell the team about this.');
-                  }
-                }
+            else {
+              // After update, actually fetch the session row from the DB so we have all values.
+              $result = $db->prepare('SELECT * FROM auth_sessions WHERE `sesskey` = :sesskey AND `time_expire` > :expire;');
+              $result->execute(array(':sesskey' => $sesskey, ':expire' => gmdate('Y-m-d H:i:s')));
+              $row = $result->fetch(PDO::FETCH_ASSOC);
+              if ($row) {
+                $session = $row;
               }
             }
           }
+          // If a verify_hash if set on a verified user, a password reset had been requested. As a login works right now, cancel that reset request by deleting the hash.
+          if (strlen(@$user['verify_hash'])) {
+            $result = $db->prepare('UPDATE `auth_users` SET `verify_hash` = \'\' WHERE `id` = :userid;');
+            if (!$result->execute(array(':userid' => $user['id']))) {
+              $utils->log('empty_vhash_failure', 'user: '.$user['id']);
+            }
+            else {
+              $user['verify_hash'] = '';
+            }
+          }
         }
         else {
-          $errors[] = _('The form you used was not valid. Possibly it has expired and you need to initiate the action again.');
+          $errors[] = _('This password is invalid or your email is not verified yet. Did you type them correctly?');
         }
       }
-      elseif (array_key_exists('reset', $_GET)) {
-        if ($session['logged_in']) {
-          $result = $db->prepare('SELECT `id`,`email` FROM `auth_users` WHERE `id` = :userid;');
-          $result->execute(array(':userid' => $session['user']));
-          $user = $result->fetch(PDO::FETCH_ASSOC);
+      else {
+        // new user: check password, create user and send verification; existing users: re-send verification or send password change instructions
+        if (array_key_exists('pwd', $_POST)) {
+          $errors += $utils->checkPasswordConstraints(strval($_POST['pwd']), $_POST['email']);
+        }
+        if (!count($errors)) {
+          // Put user into the DB
           if (!$user['id']) {
-            $utils->log('reset_user_read_failure', 'user: '.$session['user']);
+            $newHash = $utils->pwdHash($_POST['pwd']);
+            $vcode = $utils->createVerificationCode();
+            $result = $db->prepare('INSERT INTO `auth_users` (`email`, `pwdhash`, `status`, `verify_hash`) VALUES (:email, :pwdhash, \'unverified\', :vcode);');
+            if (!$result->execute(array(':email' => $_POST['email'], ':pwdhash' => $newHash, ':vcode' => $vcode))) {
+              $utils->log('user_insert_failure', 'email: '.$_POST['email']);
+              $errors[] = _('Could not add user. Please <a href="https://www.kairo.at/contact">contact KaiRo.at</a> and tell the team about this.');
+            }
+            $user = array('id' => $db->lastInsertId(),
+                          'email' => $_POST['email'],
+                          'pwdhash' => $newHash,
+                          'status' => 'unverified',
+                          'verify_hash' => $vcode);
+            $utils->log('new_user', 'user: '.$user['id'].', email: '.$user['email']);
           }
-          $pagetype = 'resetpwd';
-        }
-        else {
-          // Display form for entering email.
-          $pagetype = 'resetstart';
-        }
-      }
-      elseif (array_key_exists('verification_code', $_GET)) {
-        $result = $db->prepare('SELECT `id`,`email` FROM `auth_users` WHERE `email` = :email AND `status` = \'unverified\' AND `verify_hash` = :vcode;');
-        $result->execute(array(':email' => @$_GET['email'], ':vcode' => $_GET['verification_code']));
-        $user = $result->fetch(PDO::FETCH_ASSOC);
-        if ($user['id']) {
-          $result = $db->prepare('UPDATE `auth_users` SET `verify_hash` = \'\', `status` = \'ok\' WHERE `id` = :userid;');
-          if (!$result->execute(array(':userid' => $user['id']))) {
-            $utils->log('verification_save_failure', 'user: '.$user['id']);
-            $errors[] = _('Could not save confirmation. Please <a href="https://www.kairo.at/contact">contact KaiRo.at</a> and tell the team about this.');
+          if ($user['status'] == 'unverified') {
+            // Send email for verification and show message to point to it.
+            $mail = new email();
+            $mail->setCharset('utf-8');
+            $mail->addHeader('X-KAIRO-AUTH', 'email_verification');
+            $mail->addRecipient($user['email']);
+            $mail->setSender('noreply@auth.kairo.at', _('KaiRo.at Authentication Service'));
+            $mail->setSubject('Email Verification for KaiRo.at Authentication');
+            $mail->addMailText(_('Welcome!')."\n\n");
+            $mail->addMailText(sprintf(_('This email address, %s, has been used for registration on "%s".'),
+                                      $user['email'], _('KaiRo.at Authentication Service'))."\n\n");
+            $mail->addMailText(_('Please confirm that registration by clicking the following link (or calling it up in your browser):')."\n");
+            $mail->addMailText(($utils->running_on_localhost?'http':'https').'://'.$_SERVER['SERVER_NAME'].strstr($_SERVER['REQUEST_URI'], '?', true)
+                              .'?email='.rawurlencode($user['email']).'&verification_code='.rawurlencode($user['verify_hash'])."\n\n");
+            $mail->addMailText(_('With this confirmation, you accept that we handle your data for the purpose of logging you into other websites when you request that.')."\n");
+            $mail->addMailText(_('Those websites will get to know your email address but not your password, which we store securely.')."\n");
+            $mail->addMailText(_('If you do not call this confirmation link within 72 hours, your data will be deleted from our database.')."\n\n");
+            $mail->addMailText(sprintf(_('The %s team'), 'KaiRo.at'));
+            //$mail->setDebugAddress("robert@localhost");
+            $mailsent = $mail->send();
+            if ($mailsent) {
+              $pagetype = 'verification_sent';
+            }
+            else {
+              $utils->log('verify_mail_failure', 'user: '.$user['id'].', email: '.$user['email']);
+              $errors[] = _('The confirmation email could not be sent to you. Please <a href="https://www.kairo.at/contact">contact KaiRo.at</a> and tell the team about this.');
+            }
           }
-          $pagetype = 'verification_done';
-        }
-        else {
-          $errors[] = _('The confirmation link you called is not valid. Possibly it has expired and you need to try registering again.');
-        }
-      }
-      elseif (array_key_exists('reset_code', $_GET)) {
-        $reset_fail = true;
-        $result = $db->prepare('SELECT `id`,`email`,`verify_hash` FROM `auth_users` WHERE `email` = :email');
-        $result->execute(array(':email' => @$_GET['email']));
-        $user = $result->fetch(PDO::FETCH_ASSOC);
-        if ($user['id']) {
-          // Deconstruct reset code and verify it.
-          if (preg_match('/^([0-9a-f]{'.strlen($user['verify_hash']).'})([0-9a-f]+)_(\d+\.\d+)$/', $_GET['reset_code'], $regs)) {
-            $tcode_sessid = hexdec($regs[2]) - $user['id'];
-            $result = $db->prepare('SELECT `id`,`sesskey` FROM `auth_sessions` WHERE `id` = :sessid;');
-            $result->execute(array(':sessid' => $tcode_sessid));
-            $row = $result->fetch(PDO::FETCH_ASSOC);
-            if ($row) {
-              $tcode_session = $row;
-              if (($regs[1] == $user['verify_hash']) &&
-                  $utils->verifyTimeCode($regs[3], $session, 60)) {
-                // Set a new verify_hash for the actual password reset.
-                $user['verify_hash'] = $utils->createVerificationCode();
-                $result = $db->prepare('UPDATE `auth_users` SET `verify_hash` = :vcode WHERE `id` = :userid;');
-                if (!$result->execute(array(':vcode' => $user['verify_hash'], ':userid' => $user['id']))) {
-                  $utils->log('vhash_reset_failure', 'user: '.$user['id']);
-                }
-                $result = $db->prepare('UPDATE `auth_sessions` SET `user` = :userid WHERE `id` = :sessid;');
-                if (!$result->execute(array(':userid' => $user['id'], ':sessid' => $session['id']))) {
-                  $utils->log('reset_session_set_user_failure', 'session: '.$session['id']);
-                }
-                $pagetype = 'resetpwd';
-                $reset_fail = false;
+          else {
+            // Password reset requested with "Password forgotten?" function.
+            $vcode = $utils->createVerificationCode();
+            $result = $db->prepare('UPDATE `auth_users` SET `verify_hash` = :vcode WHERE `id` = :userid;');
+            if (!$result->execute(array(':vcode' => $vcode, ':userid' => $user['id']))) {
+              $utils->log('vhash_set_failure', 'user: '.$user['id']);
+              $errors[] = _('Could not initiate reset request. Please <a href="https://www.kairo.at/contact">contact KaiRo.at</a> and tell the team about this.');
+            }
+            else {
+              $utils->log('pwd_reset_request', 'user: '.$user['id'].', email: '.$user['email']);
+              $resetcode = $vcode.dechex($user['id'] + $session['id']).'_'.$utils->createTimeCode($session, null, 60);
+              // Send email with instructions for resetting the password.
+              $mail = new email();
+              $mail->setCharset('utf-8');
+              $mail->addHeader('X-KAIRO-AUTH', 'password_reset');
+              $mail->addRecipient($user['email']);
+              $mail->setSender('noreply@auth.kairo.at', _('KaiRo.at Authentication Service'));
+              $mail->setSubject('How to reset your password for KaiRo.at Authentication');
+              $mail->addMailText(_('Hi,')."\n\n");
+              $mail->addMailText(sprintf(_('A request for setting a new password for this email address, %s, has been submitted on "%s".'),
+                                        $user['email'], _('KaiRo.at Authentication Service'))."\n\n");
+              $mail->addMailText(_('You can set a new password by clicking the following link (or calling it up in your browser):')."\n");
+              $mail->addMailText(($utils->running_on_localhost?'http':'https').'://'.$_SERVER['SERVER_NAME'].strstr($_SERVER['REQUEST_URI'], '?', true)
+                                .'?email='.rawurlencode($user['email']).'&reset_code='.rawurlencode($resetcode)."\n\n");
+              $mail->addMailText(_('If you do not call this confirmation link within 1 hour, this link expires and the existing password is being kept in place.')."\n\n");
+              $mail->addMailText(sprintf(_('The %s team'), 'KaiRo.at'));
+              //$mail->setDebugAddress("robert@localhost");
+              $mailsent = $mail->send();
+              if ($mailsent) {
+                $pagetype = 'resetmail_sent';
+              }
+              else {
+                $utils->log('pwd_reset_mail_failure', 'user: '.$user['id'].', email: '.$user['email']);
+                $errors[] = _('The email with password reset instructions could not be sent to you. Please <a href="https://www.kairo.at/contact">contact KaiRo.at</a> and tell the team about this.');
               }
             }
           }
         }
-        if ($reset_fail) {
-          $errors[] = _('The password reset link you called is not valid. Possibly it has expired and you need to call the "Password forgotten?" function again.');
-        }
       }
-      elseif (intval($session['user'])) {
-        $result = $db->prepare('SELECT `id`,`email`,`verify_hash` FROM `auth_users` WHERE `id` = :userid;');
-        $result->execute(array(':userid' => $session['user']));
-        $user = $result->fetch(PDO::FETCH_ASSOC);
-        if (!$user['id']) {
-          $utils->log('user_read_failure', 'user: '.$session['user']);
-        }
-        // Password reset requested.
-        if (array_key_exists('pwd', $_POST) && array_key_exists('reset', $_POST) && array_key_exists('tcode', $_POST)) {
-          // If not logged in, a password reset needs to have the proper vcode set.
-          if (!$session['logged_in'] && (!strlen(@$_POST['vcode']) || ($_POST['vcode'] != $user['verify_hash']))) {
-            $errors[] = _('Password reset failed. The reset form you used was not valid. Possibly it has expired and you need to initiate the password reset again.');
-          }
-          // If not logged in, a password reset also needs to have the proper email set.
-          if (!$session['logged_in'] && !count($errors) && (@$_POST['email_hidden'] != $user['email'])) {
-            $errors[] = _('Password reset failed. The reset form you used was not valid. Possibly it has expired and you need to initiate the password reset again.');
-          }
-          // Check validity of time code.
-          if (!count($errors) && !$utils->verifyTimeCode($_POST['tcode'], $session)) {
-            $errors[] = _('Password reset failed. The reset form you used was not valid. Possibly it has expired and you need to initiate the password reset again.');
-          }
-          $errors += $utils->checkPasswordConstraints(strval($_POST['pwd']), $user['email']);
-          if (!count($errors)) {
-            $newHash = $utils->pwdHash($_POST['pwd']);
-            $result = $db->prepare('UPDATE `auth_users` SET `pwdhash` = :pwdhash, `verify_hash` = \'\' WHERE `id` = :userid;');
-            if (!$result->execute(array(':pwdhash' => $newHash, ':userid' => $session['user']))) {
-              $utils->log('pwd_reset_failure', 'user: '.$session['user']);
-              $errors[] = _('Password reset failed. Please <a href="https://www.kairo.at/contact">contact KaiRo.at</a> and tell the team about this.');
+    }
+    else {
+      $errors[] = _('The form you used was not valid. Possibly it has expired and you need to initiate the action again.');
+    }
+  }
+  elseif (array_key_exists('reset', $_GET)) {
+    if ($session['logged_in']) {
+      $result = $db->prepare('SELECT `id`,`email` FROM `auth_users` WHERE `id` = :userid;');
+      $result->execute(array(':userid' => $session['user']));
+      $user = $result->fetch(PDO::FETCH_ASSOC);
+      if (!$user['id']) {
+        $utils->log('reset_user_read_failure', 'user: '.$session['user']);
+      }
+      $pagetype = 'resetpwd';
+    }
+    else {
+      // Display form for entering email.
+      $pagetype = 'resetstart';
+    }
+  }
+  elseif (array_key_exists('verification_code', $_GET)) {
+    $result = $db->prepare('SELECT `id`,`email` FROM `auth_users` WHERE `email` = :email AND `status` = \'unverified\' AND `verify_hash` = :vcode;');
+    $result->execute(array(':email' => @$_GET['email'], ':vcode' => $_GET['verification_code']));
+    $user = $result->fetch(PDO::FETCH_ASSOC);
+    if ($user['id']) {
+      $result = $db->prepare('UPDATE `auth_users` SET `verify_hash` = \'\', `status` = \'ok\' WHERE `id` = :userid;');
+      if (!$result->execute(array(':userid' => $user['id']))) {
+        $utils->log('verification_save_failure', 'user: '.$user['id']);
+        $errors[] = _('Could not save confirmation. Please <a href="https://www.kairo.at/contact">contact KaiRo.at</a> and tell the team about this.');
+      }
+      $pagetype = 'verification_done';
+    }
+    else {
+      $errors[] = _('The confirmation link you called is not valid. Possibly it has expired and you need to try registering again.');
+    }
+  }
+  elseif (array_key_exists('reset_code', $_GET)) {
+    $reset_fail = true;
+    $result = $db->prepare('SELECT `id`,`email`,`verify_hash` FROM `auth_users` WHERE `email` = :email');
+    $result->execute(array(':email' => @$_GET['email']));
+    $user = $result->fetch(PDO::FETCH_ASSOC);
+    if ($user['id']) {
+      // Deconstruct reset code and verify it.
+      if (preg_match('/^([0-9a-f]{'.strlen($user['verify_hash']).'})([0-9a-f]+)_(\d+\.\d+)$/', $_GET['reset_code'], $regs)) {
+        $tcode_sessid = hexdec($regs[2]) - $user['id'];
+        $result = $db->prepare('SELECT `id`,`sesskey` FROM `auth_sessions` WHERE `id` = :sessid;');
+        $result->execute(array(':sessid' => $tcode_sessid));
+        $row = $result->fetch(PDO::FETCH_ASSOC);
+        if ($row) {
+          $tcode_session = $row;
+          if (($regs[1] == $user['verify_hash']) &&
+              $utils->verifyTimeCode($regs[3], $session, 60)) {
+            // Set a new verify_hash for the actual password reset.
+            $user['verify_hash'] = $utils->createVerificationCode();
+            $result = $db->prepare('UPDATE `auth_users` SET `verify_hash` = :vcode WHERE `id` = :userid;');
+            if (!$result->execute(array(':vcode' => $user['verify_hash'], ':userid' => $user['id']))) {
+              $utils->log('vhash_reset_failure', 'user: '.$user['id']);
             }
-            else {
-              $pagetype = 'reset_done';
+            $result = $db->prepare('UPDATE `auth_sessions` SET `user` = :userid WHERE `id` = :sessid;');
+            if (!$result->execute(array(':userid' => $user['id'], ':sessid' => $session['id']))) {
+              $utils->log('reset_session_set_user_failure', 'session: '.$session['id']);
             }
+            $pagetype = 'resetpwd';
+            $reset_fail = false;
           }
         }
       }
     }
+    if ($reset_fail) {
+      $errors[] = _('The password reset link you called is not valid. Possibly it has expired and you need to call the "Password forgotten?" function again.');
+    }
   }
-  if (is_null($session)) {
-    // Create new session and set cookie.
-    $sesskey = $utils->createSessionKey();
-    setcookie('sessionkey', $sesskey, 0, "", "", !$utils->running_on_localhost, true); // Last two params are secure and httponly, secure is not set on localhost.
-    $result = $db->prepare('INSERT INTO `auth_sessions` (`sesskey`, `time_expire`) VALUES (:sesskey, :expire);');
-    $result->execute(array(':sesskey' => $sesskey, ':expire' => gmdate('Y-m-d H:i:s', strtotime('+5 minutes'))));
-    // After insert, actually fetch the session row from the DB so we have all values.
-    $result = $db->prepare('SELECT * FROM auth_sessions WHERE `sesskey` = :sesskey AND `time_expire` > :expire;');
-    $result->execute(array(':sesskey' => $sesskey, ':expire' => gmdate('Y-m-d H:i:s')));
-    $row = $result->fetch(PDO::FETCH_ASSOC);
-    if ($row) {
-      $session = $row;
+  elseif (intval($session['user'])) {
+    $result = $db->prepare('SELECT `id`,`email`,`verify_hash` FROM `auth_users` WHERE `id` = :userid;');
+    $result->execute(array(':userid' => $session['user']));
+    $user = $result->fetch(PDO::FETCH_ASSOC);
+    if (!$user['id']) {
+      $utils->log('user_read_failure', 'user: '.$session['user']);
     }
-    else {
-      $utils->log('session_create_failure', 'key: '.$sesskey);
-      $errors[] = _('The session system is not working. Please <a href="https://www.kairo.at/contact">contact KaiRo.at</a> and tell the team about this.');
+    // Password reset requested.
+    if (array_key_exists('pwd', $_POST) && array_key_exists('reset', $_POST) && array_key_exists('tcode', $_POST)) {
+      // If not logged in, a password reset needs to have the proper vcode set.
+      if (!$session['logged_in'] && (!strlen(@$_POST['vcode']) || ($_POST['vcode'] != $user['verify_hash']))) {
+        $errors[] = _('Password reset failed. The reset form you used was not valid. Possibly it has expired and you need to initiate the password reset again.');
+      }
+      // If not logged in, a password reset also needs to have the proper email set.
+      if (!$session['logged_in'] && !count($errors) && (@$_POST['email_hidden'] != $user['email'])) {
+        $errors[] = _('Password reset failed. The reset form you used was not valid. Possibly it has expired and you need to initiate the password reset again.');
+      }
+      // Check validity of time code.
+      if (!count($errors) && !$utils->verifyTimeCode($_POST['tcode'], $session)) {
+        $errors[] = _('Password reset failed. The reset form you used was not valid. Possibly it has expired and you need to initiate the password reset again.');
+      }
+      $errors += $utils->checkPasswordConstraints(strval($_POST['pwd']), $user['email']);
+      if (!count($errors)) {
+        $newHash = $utils->pwdHash($_POST['pwd']);
+        $result = $db->prepare('UPDATE `auth_users` SET `pwdhash` = :pwdhash, `verify_hash` = \'\' WHERE `id` = :userid;');
+        if (!$result->execute(array(':pwdhash' => $newHash, ':userid' => $session['user']))) {
+          $utils->log('pwd_reset_failure', 'user: '.$session['user']);
+          $errors[] = _('Password reset failed. Please <a href="https://www.kairo.at/contact">contact KaiRo.at</a> and tell the team about this.');
+        }
+        else {
+          $pagetype = 'reset_done';
+        }
+      }
     }
   }
 }