extract domains from redirect URIs, fall back to client ID when that is not present
[authserver.git] / index.php
index ee03d8b034c115a935d0913eab33b04edd138733..5f091a26ae14b152d49f79e44f3f0ca145b1ea83 100644 (file)
--- a/index.php
+++ b/index.php
@@ -17,7 +17,13 @@ $head->appendJSFile('authsystem.js');
 $title->appendText('KaiRo.at Authentication Server');
 $h1 = $body->appendElement('h1', 'KaiRo.at Authentication Server');
 
+// Make the document not be scaled on mobile devices.
+$vpmeta = $head->appendElement('meta');
+$vpmeta->setAttribute('name', 'viewport');
+$vpmeta->setAttribute('content', 'width=device-width, height=device-height');
+
 $errors = $utils->checkForSecureConnection();
+$utils->sendSecurityHeaders();
 
 $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');
@@ -34,7 +40,7 @@ if (!count($errors)) {
     $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.');
+      $errors[] = _('Unexpected error while logging out.');
     }
     $session['logged_in'] = 0;
   }
@@ -43,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)) {
@@ -65,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 <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.');
-            }
-            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;');
@@ -118,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?');
@@ -135,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 <a href="https://www.kairo.at/contact">contact KaiRo.at</a> and tell the team about this.');
             }
             $user = array('id' => $db->lastInsertId(),
@@ -212,9 +180,34 @@ 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.');
+      $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.');
     }
   }
   elseif (array_key_exists('reset', $_GET)) {
@@ -284,8 +277,41 @@ if (!count($errors)) {
       $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 (array_key_exists('clients', $_GET)) {
+    $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 ($session['logged_in'] && $user['id']) {
+      if (array_key_exists('client_id', $_POST) && (strlen($_POST['client_id']) >= 5)) {
+        $clientid = $_POST['client_id'];
+        $clientsecret = $utils->createClientSecret();
+        $rediruri = strval(@$_POST['redirect_uri']);
+        $scope = strval(@$_POST['scope']);
+        $result = $db->prepare('INSERT INTO `oauth_clients` (`client_id`, `client_secret`, `redirect_uri`, `scope`, `user_id`) VALUES (:clientid, :secret, :rediruri, :scope, :userid);');
+        if (!$result->execute(array(':clientid' => $clientid,
+                                    ':secret' => $clientsecret,
+                                    ':rediruri' => $rediruri,
+                                    ':scope' => $scope,
+                                    ':userid' => $user['id']))) {
+          $utils->log('client_save_failure', 'client: '.$clientid);
+          $errors[] = 'Unexpectedly failed to save new client information. Please <a href="https://www.kairo.at/contact">contact KaiRo.at</a> and tell the team about this.';
+        }
+      }
+      if (!count($errors)) {
+        // List clients
+        $result = $db->prepare('SELECT `client_id`,`client_secret`,`redirect_uri`,`scope` FROM `oauth_clients` WHERE `user_id` = :userid;');
+        $result->execute(array(':userid' => $user['id']));
+        $clients = $result->fetchAll(PDO::FETCH_ASSOC);
+        if (!$clients) { $clients = array(); }
+        $pagetype = 'clientlist';
+      }
+    }
+    else {
+      $errors[] = _('This function is only available if you are logged in.');
+    }
+  }
   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']) {
@@ -318,6 +344,9 @@ if (!count($errors)) {
         }
       }
     }
+    else {
+      $utils->doRedirectIfSet($session);
+    }
   }
 }
 
@@ -327,11 +356,17 @@ if (!count($errors)) {
     $para->setAttribute('class', 'verifyinfo pending');
     $para = $body->appendElement('p', _('Reload this page after you confirm to continue.'));
     $para->setAttribute('class', 'verifyinfo pending');
+    $para = $body->appendElement('p');
+    $para->setAttribute('class', 'verifyinfo pending');
+    $link = $para->appendLink('./', _('Reload'));
   }
   elseif ($pagetype == 'resetmail_sent') {
     $para = $body->appendElement('p',
         _('An email has been sent to the requested account with further information. If you do not receive an email then please confirm you have entered the same email address used during account registration.'));
     $para->setAttribute('class', 'resetinfo pending');
+    $para = $body->appendElement('p');
+    $para->setAttribute('class', 'resetinfo pending small');
+    $link = $para->appendLink('./', _('Back to top'));
   }
   elseif ($pagetype == 'resetstart') {
     $para = $body->appendElement('p', _('If you forgot your password or didn\'t receive the registration confirmation, please enter your email here.'));
@@ -349,10 +384,13 @@ if (!count($errors)) {
     $litem = $ulist->appendElement('li');
     $litem->appendInputHidden('tcode', $utils->createTimeCode($session));
     $submit = $litem->appendInputSubmit(_('Send instructions to email'));
+    $para = $form->appendElement('p');
+    $para->setAttribute('class', 'toplink small');
+    $link = $para->appendLink('./', _('Cancel'));
   }
   elseif ($pagetype == 'resetpwd') {
     $para = $body->appendElement('p', sprintf(_('You can set a new password for %s here.'), $user['email']));
-    $para->setAttribute('class', '');
+    $para->setAttribute('class', 'newpwdinfo');
     $form = $body->appendForm('./', 'POST', 'newpwdform');
     $form->setAttribute('id', 'loginform');
     $form->setAttribute('class', 'loginarea hidden');
@@ -375,14 +413,64 @@ if (!count($errors)) {
       $litem->appendInputHidden('vcode', $user['verify_hash']);
     }
     $submit = $litem->appendInputSubmit(_('Save password'));
+    $para = $form->appendElement('p');
+    $para->setAttribute('class', 'toplink small');
+    $link = $para->appendLink('./', _('Cancel'));
+  }
+  elseif ($pagetype == 'clientlist') {
+    $scopes = array('clientreg', 'email');
+    $form = $body->appendForm('?clients', 'POST', 'newclientform');
+    $form->setAttribute('id', 'clientform');
+    $tbl = $form->appendElement('table');
+    $tbl->setAttribute('class', 'clientlist border');
+    $thead = $tbl->appendElement('thead');
+    $trow = $thead->appendElement('tr');
+    $trow->appendElement('th', _('Client ID'));
+    $trow->appendElement('th', _('Client Secrect'));
+    $trow->appendElement('th', _('Redirect URI'));
+    $trow->appendElement('th', _('Scope'));
+    $trow->appendElement('th');
+    $tbody = $tbl->appendElement('tbody');
+    foreach ($clients as $client) {
+      $trow = $tbody->appendElement('tr');
+      $trow->appendElement('td', $client['client_id']);
+      $trow->appendElement('td', $client['client_secret']);
+      $trow->appendElement('td', $client['redirect_uri']);
+      $trow->appendElement('td', $client['scope']);
+      $trow->appendElement('td'); // Future: Delete link?
+    }
+    // Form fields for adding a new one.
+    $tfoot = $tbl->appendElement('tfoot');
+    $trow = $tfoot->appendElement('tr');
+    $cell = $trow->appendElement('td');
+    $inptxt = $cell->appendInputText('client_id', 80, 25, 'client_id');
+    $cell = $trow->appendElement('td'); // Empty, as secret will be generated.
+    $cell = $trow->appendElement('td');
+    $inptxt = $cell->appendInputText('redirect_uri', 500, 50, 'redirect_uri');
+    $cell = $trow->appendElement('td');
+    $select = $cell->appendElementSelect('scope');
+    foreach ($scopes as $scope) {
+      $select->appendElementOption($scope, $scope);
+    }
+    //$inptxt = $cell->appendInputText('scope', 100, 20, 'scope');
+    $cell = $trow->appendElement('td');
+    $submit = $cell->appendInputSubmit(_('Create'));
+    $para = $form->appendElement('p');
+    $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');
@@ -390,9 +478,16 @@ if (!count($errors)) {
     $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'));
+    }
+    $litem = $ulist->appendElement('li');
     $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');
@@ -401,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);
   }
 }