From 60e46184b54841fe5dd742e85ddc61727f809baa Mon Sep 17 00:00:00 2001 From: Robert Kaiser Date: Wed, 16 Nov 2016 20:29:13 +0100 Subject: [PATCH] KaiRo bug 393 - Create a grouping mechanism for user names so people with multiple logins can chose; also move some session handling functions into the utilities --- authorize.php | 32 ++++++++---- authsystem.css | 12 +++-- authsystem.inc.php | 1 + authsystem.js | 2 +- authutils.php-class | 121 ++++++++++++++++++++++++++++++++++++++++++-- index.php | 99 ++++++++++++++++++------------------ 6 files changed, 201 insertions(+), 66 deletions(-) diff --git a/authorize.php b/authorize.php index 114064f..b434496 100644 --- a/authorize.php +++ b/authorize.php @@ -39,7 +39,7 @@ if (!count($errors)) { $session['logged_in'] = 0; } if (intval($session['user'])) { - $result = $db->prepare('SELECT `id`,`email`,`verify_hash` FROM `auth_users` WHERE `id` = :userid;'); + $result = $db->prepare('SELECT `id`,`email`,`verify_hash`,`group_id` FROM `auth_users` WHERE `id` = :userid;'); $result->execute(array(':userid' => $session['user'])); $user = $result->fetch(PDO::FETCH_ASSOC); if (!$user['id']) { @@ -84,8 +84,15 @@ if (!count($errors)) { $form = $body->appendForm('', 'POST', 'loginauthform'); $form->setAttribute('id', 'loginauthform'); $form->setAttribute('class', 'loginarea'); - $form->appendInputRadio('user_email', 'uemail_'.md5($user['email']), $user['email'], $user['email'] == $user['email']); - $form->appendLabel('uemail_'.md5($user['email']), $user['email']); + $ulist = $form->appendElement('ul'); + $ulist->setAttribute('class', 'flat emaillist'); + $emails = $utils->getGroupedEmails($user['group_id']); + if (!count($emails)) { $emails = array($user['email']); } + foreach ($emails as $email) { + $litem = $ulist->appendElement('li'); + $litem->appendInputRadio('user_email', 'uemail_'.md5($email), $email, $email == $user['email']); + $litem->appendLabel('uemail_'.md5($email), $email); + } $para = $form->appendElement('p'); $para->setAttribute('class', 'small otheremaillinks'); $link = $para->appendLink('#', _('Add another email address')); @@ -100,8 +107,19 @@ if (!count($errors)) { $para->setAttribute('class', 'small'); $link = $para->appendLink('#', _('Cancel')); $link->setAttribute('id', 'cancelauth'); // Makes the JS put the right functionality onto the link. + $utils->setRedirect($session, $_SERVER['REQUEST_URI']); } else { + // Switch to different user if we selected a different email within the group. + if (strlen(@$_POST['user_email']) && ($_POST['user_email'] != $user['email'])) { + $result = $db->prepare('SELECT `id`, `pwdhash`, `email`, `status`, `verify_hash`,`group_id` FROM `auth_users` WHERE `group_id` = :groupid AND `email` = :email;'); + $result->execute(array(':groupid' => $user['group_id'], ':email' => $_POST['user_email'])); + $newuser = $result->fetch(PDO::FETCH_ASSOC); + if ($newuser) { + $user = $newuser; + $session = $utils->getLoginSession($user['id'], $session); + } + } // Handle authorize request, forwarding code in GET parameters if the user has authorized your client. $is_authorized = (@$_POST['authorized'] === 'yes'); $server->handleAuthorizeRequest($request, $response, $is_authorized, $user['id']); @@ -112,6 +130,7 @@ if (!count($errors)) { exit("SUCCESS! Authorization Code: $code"); } */ + $utils->resetRedirect($session); $response->send(); exit(); } @@ -121,12 +140,7 @@ if (!count($errors)) { $para = $body->appendElement('p', _('You need to log in or register to continue.')); $para->setAttribute('class', 'logininfo'); $utils->appendLoginForm($body, $session, $user); - // Save the request in the session so we can get back to fulfilling it. - $result = $db->prepare('UPDATE `auth_sessions` SET `saved_redirect` = :redir WHERE `id` = :sessid;'); - $saved_request = str_replace('&logout=1', '', $_SERVER['REQUEST_URI']); // Make sure to strip a logout to not get into a loop. - if (!$result->execute(array(':redir' => $saved_request, ':sessid' => $session['id']))) { - $utils->log('redir_save_failure', 'session: '.$session['id'].', redirect: '.$saved_request); - } + $utils->setRedirect($session, str_replace('&logout=1', '', $_SERVER['REQUEST_URI'])); // Make sure to strip a logout to not get into a loop. } } diff --git a/authsystem.css b/authsystem.css index 4a273f9..4c8dd5f 100644 --- a/authsystem.css +++ b/authsystem.css @@ -94,6 +94,12 @@ table.border td { margin: 0.5rem 1rem 0; } +.loginheader > .groupmails { + font-weight: normal; + margin: 5px 0; /* IE8 and older do not support rem */ + margin: 0.5rem 0; +} + .loginlinks { margin: 5px 10px 0; /* IE8 and older do not support rem */ margin: 0.5rem 1rem 0; @@ -106,6 +112,7 @@ table.border td { .resetinfo, .verifyinfo, +.addemailinfo, .newpwdinfo, .signinwelcome { margin: 5px 10px 0; /* IE8 and older do not support rem */ @@ -133,11 +140,6 @@ table.border td { .otheremaillinks > a:link, .otheremaillinks > a:visited { color: #BBBBBB; } .otheremaillinks > a:hover, .otheremaillinks > a:active { color: #808080; } -#addanotheremail { /* HACK - not implemented yet */ - background-color: transparent !important; - color: transparent !important; -} - .small { font-size: 0.75em; } diff --git a/authsystem.inc.php b/authsystem.inc.php index 5354cd9..17818ef 100644 --- a/authsystem.inc.php +++ b/authsystem.inc.php @@ -53,6 +53,7 @@ CREATE TABLE `auth_users` ( `pwdhash` VARCHAR(255) NOT NULL , `status` ENUM('unverified','ok') NOT NULL DEFAULT 'unverified' , `verify_hash` VARCHAR(150) NULL DEFAULT NULL , + `group_id` MEDIUMINT UNSIGNED DEFAULT '0' , PRIMARY KEY (`id`), UNIQUE (`email`) ); diff --git a/authsystem.js b/authsystem.js index dee8142..7a24d84 100644 --- a/authsystem.js +++ b/authsystem.js @@ -33,7 +33,7 @@ window.onload = function() { var addAnotherEmail = document.getElementById("addanotheremail"); if (addAnotherEmail) { addAnotherEmail.onclick = function() { - // Not implemented yet. + location.href = "./?addemail"; } } var isNotMe = document.getElementById("isnotme"); diff --git a/authutils.php-class b/authutils.php-class index e72768a..9d33f26 100755 --- a/authutils.php-class +++ b/authutils.php-class @@ -37,6 +37,18 @@ class AuthUtils { // function initSession() // Initialize a session. Returns an associative array of all the DB fields of the session. // + // function getLoginSession($user) + // Return an associative array of a session with the given user logged in (new if user changed compared to given previous session, otherwise updated variant of that previous session). + // + // function setRedirect($session, $redirect) + // Set a redirect on the session for performing later. Returns true if a redirect was saved, otherwise false. + // + // function doRedirectIfSet($session) + // If the session has a redirect set, perform it. Returns true if a redirect was performed, otherwise false. + // + // function resetRedirect($session) + // If the session has a redirect set, remove it. Returns true if a redirect was removed, otherwise false. + // // function getDomainBaseURL() // Get the base URL of the current domain, e.g. 'https://example.com'. // @@ -70,8 +82,12 @@ class AuthUtils { // function pwdNeedsRehash($user) // Return true if the pwdhash field of the user uses an outdated standard and needs to be rehashed. // - // function appendLoginForm($dom_element, $session, $user) - // append a login form for the given session to the given DOM element, possibly prefilling the email from the given user info array. + // function getGroupedEmails($group_id, [$exclude_email]) + // Return all emails grouped in the specified group ID, optionally exclude a specific email (e.g. because you only want non-current entries) + // + // function appendLoginForm($dom_element, $session, $user, [$addfields]) + // Append a login form for the given session to the given DOM element, possibly prefilling the email from the given user info array. + // The optional $addfields parameter is an array of name=>value pairs of hidden fields to add to the form. function __construct($settings, $db) { // *** constructor *** @@ -159,6 +175,90 @@ class AuthUtils { return $session; } + function getLoginSession($userid, $prev_session) { + $session = $prev_session; + $sesskey = $this->createSessionKey(); + setcookie('sessionkey', $sesskey, 0, "", "", !$this->running_on_localhost, true); // Last two params are secure and httponly, secure is not set on localhost. + // If the previous session has a user set, create a new one - otherwise take existing session entry. + if (intval($session['user'])) { + $result = $this->db->prepare('INSERT INTO `auth_sessions` (`sesskey`, `time_expire`, `user`, `logged_in`) VALUES (:sesskey, :expire, :userid, TRUE);'); + $result->execute(array(':sesskey' => $sesskey, ':userid' => $userid, ':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 = $this->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: '.$userid); + $errors[] = _('The session system is not working. Please contact KaiRo.at and tell the team about this.'); + } + } + else { + $result = $this->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' => $userid, ':expire' => gmdate('Y-m-d H:i:s', strtotime('+1 day')), ':sessid' => $session['id']))) { + $utils->log('login_failure', 'session: '.$session['id'].', user: '.$userid); + $errors[] = _('Login failed unexpectedly. Please contact KaiRo.at and tell the team about this.'); + } + else { + // After update, actually fetch the session row from the DB so we have all values. + $result = $this->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; + } + } + } + return $session; + } + + function setRedirect($session, $redirect) { + $success = false; + // Save the request in the session so we can get back to fulfilling it if one of the links is clicked. + $result = $this->db->prepare('UPDATE `auth_sessions` SET `saved_redirect` = :redir WHERE `id` = :sessid;'); + if (!$result->execute(array(':redir' => $redirect, ':sessid' => $session['id']))) { + $this->log('redir_save_failure', 'session: '.$session['id'].', redirect: '.$redirect); + } + else { + $success = true; + } + return $success; + } + + function doRedirectIfSet($session) { + $success = false; + // If the session has a redirect set, make sure it's performed. + if (strlen(@$session['saved_redirect'])) { + // Remove redirect. + $result = $this->db->prepare('UPDATE `auth_sessions` SET `saved_redirect` = :redir WHERE `id` = :sessid;'); + if (!$result->execute(array(':redir' => '', ':sessid' => $session['id']))) { + $this->log('redir_save_failure', 'session: '.$session['id'].', redirect: (empty)'); + } + else { + $success = true; + } + header('Location: '.$this->getDomainBaseURL().$session['saved_redirect']); + } + return $success; + } + + function resetRedirect($session) { + $success = false; + // If the session has a redirect set, remove it. + if (strlen(@$session['saved_redirect'])) { + $result = $this->db->prepare('UPDATE `auth_sessions` SET `saved_redirect` = :redir WHERE `id` = :sessid;'); + if (!$result->execute(array(':redir' => '', ':sessid' => $session['id']))) { + $this->log('redir_save_failure', 'session: '.$session['id'].', redirect: (empty)'); + } + else { + $success = true; + } + } + return $success; + } + function getDomainBaseURL() { return ($this->running_on_localhost?'http':'https').'://'.$_SERVER['SERVER_NAME']; } @@ -253,7 +353,19 @@ class AuthUtils { } } - function appendLoginForm($dom_element, $session, $user) { + function getGroupedEmails($group_id, $exclude_email = '') { + $emails = array(); + if (intval($group_id)) { + $result = $this->db->prepare('SELECT `email` FROM `auth_users` WHERE `group_id` = :groupid AND `status` = \'ok\' AND `email` != :excludemail ORDER BY `email` ASC;'); + $result->execute(array(':groupid' => $group_id, ':excludemail' => $exclude_email)); + foreach ($result->fetchAll(PDO::FETCH_ASSOC) as $row) { + $emails[] = $row['email']; + } + } + return $emails; + } + + function appendLoginForm($dom_element, $session, $user, $addfields = array()) { $form = $dom_element->appendForm('./', 'POST', 'loginform'); $form->setAttribute('id', 'loginform'); $form->setAttribute('class', 'loginarea hidden'); @@ -280,6 +392,9 @@ class AuthUtils { $label->setAttribute('class', 'loginprompt'); $litem = $ulist->appendElement('li'); $litem->appendInputHidden('tcode', $this->createTimeCode($session)); + foreach ($addfields as $fname => $fvalue) { + $litem->appendInputHidden($fname, $fvalue); + } $submit = $litem->appendInputSubmit(_('Log in / Register')); $submit->setAttribute('class', 'loginbutton'); } diff --git a/index.php b/index.php index 23a045d..5f091a2 100644 --- a/index.php +++ b/index.php @@ -49,9 +49,11 @@ if (!count($errors)) { $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 = $db->prepare('SELECT `id`, `pwdhash`, `email`, `status`, `verify_hash`,`group_id` FROM `auth_users` WHERE `email` = :email;'); $result->execute(array(':email' => $_POST['email'])); $user = $result->fetch(PDO::FETCH_ASSOC); + // If we need to add the email to a group, note here which user's group we should be added to - otherwise, set to 0. + $addgroup = (array_key_exists('grouptoexisting', $_POST) && intval($session['user']) && ($session['user'] != @$user['id'])) ? $session['user'] : 0; if ($user['id'] && array_key_exists('pwd', $_POST)) { // existing user, check password if (($user['status'] == 'ok') && $utils->pwdVerify(@$_POST['pwd'], $user)) { @@ -71,49 +73,8 @@ if (!count($errors)) { // 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 redirect set, make sure it's performed. - if (strlen(@$session['saved_redirect'])) { - header('Location: '.$utils->getDomainBaseURL().$session['saved_redirect']); - // Remove redirect. - $result = $db->prepare('UPDATE `auth_sessions` SET `saved_redirect` = :redir WHERE `id` = :sessid;'); - if (!$result->execute(array(':redir' => '', ':sessid' => $session['id']))) { - $utils->log('redir_save_failure', 'session: '.$session['id'].', redirect: (empty)'); - } - } - // 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 contact KaiRo.at 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 contact KaiRo.at 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; - } - } - } + $prev_session = $session; + $session = $utils->getLoginSession($user['id'], $session); // 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;'); @@ -124,6 +85,7 @@ if (!count($errors)) { $user['verify_hash'] = ''; } } + $utils->doRedirectIfSet($prev_session); } else { $errors[] = _('This password is invalid or your email is not verified yet. Did you type them correctly?'); @@ -141,7 +103,7 @@ if (!count($errors)) { $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']); + $utils->log('user_insert_failure', 'email: '.$_POST['email'].' - '.$result->errorInfo()[2]); $errors[] = _('Could not add user. Please contact KaiRo.at and tell the team about this.'); } $user = array('id' => $db->lastInsertId(), @@ -218,6 +180,31 @@ if (!count($errors)) { } } } + if (!count($errors) && ($addgroup > 0)) { + // We should add the login email to the group of that existing user. + $result = $db->prepare('SELECT `group_id` FROM `auth_users` WHERE `id` = :userid;'); + $result->execute(array(':userid' => $addgroup)); + $grpuser = $result->fetch(PDO::FETCH_ASSOC); + if (!intval($grpuser['group_id'])) { + // If that user doesn't have a group, put him into a group with his own user ID. + $result = $db->prepare('UPDATE `auth_users` SET `group_id` = :groupid WHERE `id` = :userid;'); + if (!$result->execute(array(':groupid' => $addgroup, ':userid' => $addgroup))) { + $utils->log('group_save_failure', 'user: '.$addgroup); + } + else { + $utils->log('new grouping', 'user: '.$addgroup.', group: '.$addgroup); + } + } + // Save grouping for the new or logged-in user. + $result = $db->prepare('UPDATE `auth_users` SET `group_id` = :groupid WHERE `id` = :userid;'); + if (!$result->execute(array(':groupid' => $addgroup, ':userid' => $user['id']))) { + $utils->log('group_save_failure', 'user: '.$user['id']); + } + else { + $utils->log('new grouping', 'user: '.$user['id'].', group: '.$addgroup); + $user['group_id'] = $addgroup; + } + } } else { $errors[] = _('The form you used was not valid. Possibly it has expired and you need to initiate the action again, or you have disabled cookies for this site.'); @@ -324,7 +311,7 @@ if (!count($errors)) { } } elseif (intval($session['user'])) { - $result = $db->prepare('SELECT `id`,`email`,`verify_hash` FROM `auth_users` WHERE `id` = :userid;'); + $result = $db->prepare('SELECT `id`,`email`,`verify_hash`,`group_id` FROM `auth_users` WHERE `id` = :userid;'); $result->execute(array(':userid' => $session['user'])); $user = $result->fetch(PDO::FETCH_ASSOC); if (!$user['id']) { @@ -357,6 +344,9 @@ if (!count($errors)) { } } } + else { + $utils->doRedirectIfSet($session); + } } } @@ -469,19 +459,26 @@ if (!count($errors)) { $para->setAttribute('class', 'toplink'); $link = $para->appendLink('./', _('Back to top')); } - elseif ($session['logged_in']) { + elseif ($session['logged_in'] && (!array_key_exists('addemail', $_GET))) { if ($pagetype == 'reset_done') { $para = $body->appendElement('p', _('Your password has successfully been reset.')); $para->setAttribute('class', 'resetinfo done'); } $div = $body->appendElement('div', $user['email']); $div->setAttribute('class', 'loginheader'); + $groupmails = $utils->getGroupedEmails($user['group_id'], $user['email']); + if (count($groupmails)) { + $para = $div->appendElement('p', _('Grouped with: ').implode(', ', $groupmails)); + $para->setAttribute('class', 'small groupmails'); + } $div = $body->appendElement('div'); $div->setAttribute('class', 'loginlinks'); $ulist = $div->appendElement('ul'); $ulist->setAttribute('class', 'flat'); $litem = $ulist->appendElement('li'); $link = $litem->appendLink('./?logout', _('Log out')); + $litem = $ulist->appendElement('li'); + $link = $litem->appendLink('./?addemail', _('Add another email address')); if (in_array($user['email'], $utils->client_reg_email_whitelist)) { $litem = $ulist->appendElement('li'); $link = $litem->appendLink('./?clients', _('Manage OAuth2 clients')); @@ -490,6 +487,7 @@ if (!count($errors)) { $litem->appendLink('./?reset', _('Set new password')); } else { // not logged in + $addfields = array(); if ($pagetype == 'verification_done') { $para = $body->appendElement('p', _('Hooray! Your email was successfully confirmed! You can log in now.')); $para->setAttribute('class', 'verifyinfo done'); @@ -498,7 +496,12 @@ if (!count($errors)) { $para = $body->appendElement('p', _('Your password has successfully been reset. You can log in now with the new password.')); $para->setAttribute('class', 'resetinfo done'); } - $utils->appendLoginForm($body, $session, $user); + elseif (array_key_exists('addemail', $_GET)) { + $para = $body->appendElement('p', sprintf(_('Add another email grouped with %s by either logging in with it or specifying the email and a new password to use.'), $user['email'])); + $para->setAttribute('class', 'addemailinfo'); + $addfields['grouptoexisting'] = '1'; + } + $utils->appendLoginForm($body, $session, $user, $addfields); } } -- 2.43.0