| 1 | <?php |
| 2 | /* This Source Code Form is subject to the terms of the Mozilla Public |
| 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
| 4 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
| 5 | |
| 6 | // Include the common auth system files (including the OAuth2 Server object). |
| 7 | require_once(__DIR__.'/authsystem.inc.php'); |
| 8 | |
| 9 | $errors = array(); |
| 10 | |
| 11 | // Start HTML document as a DOM object. |
| 12 | extract(ExtendedDocument::initHTML5()); // sets $document, $html, $head, $title, $body |
| 13 | $document->formatOutput = true; // we want a nice output |
| 14 | |
| 15 | $style = $head->appendElement('link'); |
| 16 | $style->setAttribute('rel', 'stylesheet'); |
| 17 | $style->setAttribute('href', 'authsystem.css'); |
| 18 | $head->appendJSFile('authsystem.js'); |
| 19 | $title->appendText('KaiRo.at Authentication Server'); |
| 20 | $h1 = $body->appendElement('h1', 'KaiRo.at Authentication Server'); |
| 21 | |
| 22 | $running_on_localhost = preg_match('/^((.+\.)?localhost|127\.0\.0\.\d+)$/', $_SERVER['SERVER_NAME']); |
| 23 | if (($_SERVER['SERVER_PORT'] != 443) && !$running_on_localhost) { |
| 24 | $errors[] = _('You are not accessing this site on a secure connection, so authentication doesn\'t work.'); |
| 25 | } |
| 26 | |
| 27 | $para = $body->appendElement('p', _('This login system does not work without JavaScript. Please activate JavaScript for this site to log in.')); |
| 28 | $para->setAttribute('id', 'jswarning'); |
| 29 | $para->setAttribute('class', 'warn'); |
| 30 | |
| 31 | if (!count($errors)) { |
| 32 | $session = null; |
| 33 | $user = array('id' => 0, 'email' => ''); |
| 34 | $pagetype = 'default'; |
| 35 | $db->exec("SET time_zone='+00:00';"); // Execute directly on PDO object, set session to UTC to make our gmdate() values match correctly. |
| 36 | if (strlen(@$_COOKIE['sessionkey'])) { |
| 37 | // Fetch the session - or at least try to. |
| 38 | $result = $db->prepare('SELECT * FROM `auth_sessions` WHERE `sesskey` = :sesskey AND `time_expire` > :expire;'); |
| 39 | $result->execute(array(':sesskey' => $_COOKIE['sessionkey'], ':expire' => gmdate('Y-m-d H:i:s'))); |
| 40 | $row = $result->fetch(PDO::FETCH_ASSOC); |
| 41 | if ($row) { |
| 42 | $session = $row; |
| 43 | |
| 44 | if (array_key_exists('logout', $_GET)) { |
| 45 | $result = $db->prepare('UPDATE `auth_sessions` SET `logged_in` = FALSE WHERE `id` = :sessid;'); |
| 46 | if (!$result->execute(array(':sessid' => $session['id']))) { |
| 47 | // XXXlog: Unexpected logout failure! |
| 48 | $errors[] = _('The email address is invalid.'); |
| 49 | } |
| 50 | $session['logged_in'] = 0; |
| 51 | } |
| 52 | elseif (array_key_exists('email', $_POST)) { |
| 53 | if (!preg_match('/^[^@]+@[^@]+\.[^@]+$/', $_POST['email'])) { |
| 54 | $errors[] = _('The email address is invalid.'); |
| 55 | } |
| 56 | else { |
| 57 | $result = $db->prepare('SELECT `id`, `pwdhash`, `email`, `status`, `verify_hash` FROM `auth_users` WHERE `email` = :email;'); |
| 58 | $result->execute(array(':email' => $_POST['email'])); |
| 59 | $user = $result->fetch(PDO::FETCH_ASSOC); |
| 60 | if ($user['id'] && array_key_exists('pwd', $_POST)) { |
| 61 | // existing user, check password |
| 62 | if (($user['status'] == 'ok') && password_verify(@$_POST['pwd'], $user['pwdhash'])) { |
| 63 | // Check if a newer hashing algorithm is available |
| 64 | // or the cost has changed |
| 65 | if (password_needs_rehash($user['pwdhash'], PASSWORD_DEFAULT, $pwd_options)) { |
| 66 | // If so, create a new hash, and replace the old one |
| 67 | $newHash = password_hash($_POST['pwd'], PASSWORD_DEFAULT, $pwd_options); |
| 68 | $result = $db->prepare('UPDATE `auth_users` SET `pwdhash` = :pwdhash WHERE `id` = :userid;'); |
| 69 | if (!$result->execute(array(':pwdhash' => $newHash, ':userid' => $user['id']))) { |
| 70 | // XXXlog: Failed to update user hash! |
| 71 | } |
| 72 | } |
| 73 | |
| 74 | // Log user in - update session key for that, see https://wiki.mozilla.org/WebAppSec/Secure_Coding_Guidelines#Login |
| 75 | $sesskey = bin2hex(openssl_random_pseudo_bytes(512/8)); // Get 512 bits of randomness (128 byte hex string). |
| 76 | setcookie('sessionkey', $sesskey, 0, "", "", !$running_on_localhost, true); // Last two params are secure and httponly, secure is not set on localhost. |
| 77 | // If the session has a user set, create a new one - otherwise take existing session entry. |
| 78 | if (intval($session['user'])) { |
| 79 | $result = $db->prepare('INSERT INTO `auth_sessions` (`sesskey`, `time_expire`, `user`, `logged_in`) VALUES (:sesskey, :expire, :userid, TRUE);'); |
| 80 | $result->execute(array(':sesskey' => $sesskey, ':userid' => $user['id'], ':expire' => gmdate('Y-m-d H:i:s', strtotime('+1 day')))); |
| 81 | // After insert, actually fetch the session row from the DB so we have all values. |
| 82 | $result = $db->prepare('SELECT * FROM auth_sessions WHERE `sesskey` = :sesskey AND `time_expire` > :expire;'); |
| 83 | $result->execute(array(':sesskey' => $sesskey, ':expire' => gmdate('Y-m-d H:i:s'))); |
| 84 | $row = $result->fetch(PDO::FETCH_ASSOC); |
| 85 | if ($row) { |
| 86 | $session = $row; |
| 87 | } |
| 88 | else { |
| 89 | // XXXlog: unexpected failure to create session! |
| 90 | $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.'); |
| 91 | } |
| 92 | } |
| 93 | else { |
| 94 | $result = $db->prepare('UPDATE `auth_sessions` SET `sesskey` = :sesskey, `user` = :userid, `logged_in` = TRUE, `time_expire` = :expire WHERE `id` = :sessid;'); |
| 95 | if (!$result->execute(array(':sesskey' => $sesskey, ':userid' => $user['id'], ':expire' => gmdate('Y-m-d H:i:s', strtotime('+1 day')), ':sessid' => $session['id']))) { |
| 96 | // XXXlog: Unexpected login failure! |
| 97 | $errors[] = _('Login failed unexpectedly. Please <a href="https://www.kairo.at/contact">contact KaiRo.at</a> and tell the team about this.'); |
| 98 | } |
| 99 | } |
| 100 | } |
| 101 | else { |
| 102 | $errors[] = _('This password is invalid or your email is not verified yet. Did you type them correctly?'); |
| 103 | } |
| 104 | } |
| 105 | else { |
| 106 | // new user: check password, create user and send verification; existing users: re-send verification or send password change instructions |
| 107 | if (array_key_exists('pwd', $_POST)) { |
| 108 | $new_password = strval($_POST['pwd']); |
| 109 | if ($new_password != trim($new_password)) { |
| 110 | $errors[] = _('Password must not start or end with a whitespace character like a space.'); |
| 111 | } |
| 112 | if (strlen($new_password) < 8) { $errors[] = sprintf(_('Password too short (min. %s characters).'), 8); } |
| 113 | if (strlen($new_password) > 70) { $errors[] = sprintf(_('Password too long (max. %s characters).'), 70); } |
| 114 | if (strtolower($new_password) == strtolower($_POST['email'])) { |
| 115 | $errors[] = _('The passwort can not be equal to your email.'); |
| 116 | } |
| 117 | if ((strlen($new_password) < 15) && (preg_match('/^[a-zA-Z]*$/', $new_password))) { |
| 118 | $errors[] = sprintf(_('Your password must use letters other than normal characters or contain least 15 characters.'), 15); |
| 119 | } |
| 120 | if (strlen(count_chars($new_password, 3)) < 5) { |
| 121 | $errors[] = sprintf(_('Password does have to contain at least %s different characters.'), 5); |
| 122 | } |
| 123 | } |
| 124 | if (!count($errors)) { |
| 125 | // Put user into the DB |
| 126 | if (!$user['id']) { |
| 127 | $newHash = password_hash($_POST['pwd'], PASSWORD_DEFAULT, $pwd_options); |
| 128 | $vcode = bin2hex(openssl_random_pseudo_bytes(512/8)); // Get 512 bits of randomness (128 byte hex string). |
| 129 | $result = $db->prepare('INSERT INTO `auth_users` (`email`, `pwdhash`, `status`, `verify_hash`) VALUES (:email, :pwdhash, \'unverified\', :vcode);'); |
| 130 | if (!$result->execute(array(':email' => $_POST['email'], ':pwdhash' => $newHash, ':vcode' => $vcode))) { |
| 131 | // XXXlog: User insertion failure! |
| 132 | $errors[] = _('Could not add user. Please <a href="https://www.kairo.at/contact">contact KaiRo.at</a> and tell the team about this.'); |
| 133 | } |
| 134 | $user = array('id' => $db->lastInsertId(), |
| 135 | 'email' => $_POST['email'], |
| 136 | 'pwdhash' => $newHash, |
| 137 | 'status' => 'unverified', |
| 138 | 'verify_hash' => $vcode); |
| 139 | } |
| 140 | if ($user['status'] == 'unverified') { |
| 141 | // Send email for verification and show message to point to it. |
| 142 | $mail = new email(); |
| 143 | $mail->setCharset('utf-8'); |
| 144 | $mail->addHeader('X-KAIRO-AUTH', 'email_verification'); |
| 145 | $mail->addRecipient($user['email']); |
| 146 | $mail->setSender('noreply@auth.kairo.at', _('KaiRo.at Authentication Service')); |
| 147 | $mail->setSubject('Email Verification for KaiRo.at Authentication'); |
| 148 | $mail->addMailText(_('Welcome!')."\n\n"); |
| 149 | $mail->addMailText(sprintf(_('This email address, %s, has been used for registration on "%s".'), |
| 150 | $user['email'], _('KaiRo.at Authentication Service'))."\n\n"); |
| 151 | $mail->addMailText(_('Please confirm that registration by clicking the following link (or calling it up in your browser):')."\n"); |
| 152 | $mail->addMailText(($running_on_localhost?'http':'https').'://'.$_SERVER['SERVER_NAME'].strstr($_SERVER['REQUEST_URI'], '?', true) |
| 153 | .'?email='.rawurlencode($user['email']).'&verification_code='.rawurlencode($user['verify_hash'])."\n\n"); |
| 154 | $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"); |
| 155 | $mail->addMailText(_('Those websites will get to know your email address but not your password, which we store securely.')."\n"); |
| 156 | $mail->addMailText(_('If you do not call this confirmation link within 72 hours, your data will be deleted from our database.')."\n\n"); |
| 157 | $mail->addMailText(sprintf(_('The %s team'), 'KaiRo.at')); |
| 158 | //$mail->setDebugAddress("robert@localhost"); |
| 159 | $mailsent = $mail->send(); |
| 160 | if ($mailsent) { |
| 161 | $pagetype = 'verification_sent'; |
| 162 | } |
| 163 | else { |
| 164 | $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.'); |
| 165 | } |
| 166 | } |
| 167 | else { |
| 168 | // Send email with instructions for resetting the password. |
| 169 | } |
| 170 | } |
| 171 | } |
| 172 | } |
| 173 | } |
| 174 | elseif (array_key_exists('reset', $_GET)) { |
| 175 | if ($session['logged_in']) { |
| 176 | $pagetype = 'resetpwd'; |
| 177 | } |
| 178 | else { |
| 179 | // Display form for entering email. |
| 180 | $pagetype = 'resetstart'; |
| 181 | } |
| 182 | } |
| 183 | elseif (array_key_exists('verification_code', $_GET)) { |
| 184 | $result = $db->prepare('SELECT `id`,`email` FROM `auth_users` WHERE `email` = :email AND `status` = \'unverified\' AND `verify_hash` = :vcode;'); |
| 185 | $result->execute(array(':email' => @$_GET['email'], ':vcode' => $_GET['verification_code'])); |
| 186 | $user = $result->fetch(PDO::FETCH_ASSOC); |
| 187 | if ($user['id']) { |
| 188 | $result = $db->prepare('UPDATE `auth_users` SET `verify_hash` = \'\', `status` = \'ok\' WHERE `id` = :userid;'); |
| 189 | if (!$result->execute(array(':userid' => $user['id']))) { |
| 190 | // XXXlog: unexpected failure to save verification! |
| 191 | $errors[] = _('Could not save confirmation. Please <a href="https://www.kairo.at/contact">contact KaiRo.at</a> and tell the team about this.'); |
| 192 | } |
| 193 | $pagetype = 'verification_done'; |
| 194 | } |
| 195 | else { |
| 196 | $errors[] = _('The confirmation link you called is not valid. Possibly it has expired and you need to try registering again.'); |
| 197 | } |
| 198 | } |
| 199 | elseif (intval($session['user'])) { |
| 200 | $result = $db->prepare('SELECT `id`,`email` FROM `auth_users` WHERE `id` = :userid;'); |
| 201 | $result->execute(array(':userid' => $session['user'])); |
| 202 | $user = $result->fetch(PDO::FETCH_ASSOC); |
| 203 | if (!$user['id']) { |
| 204 | // XXXlog: unexpected failure to fetch user data! |
| 205 | } |
| 206 | } |
| 207 | } |
| 208 | } |
| 209 | if (is_null($session)) { |
| 210 | // Create new session and set cookie. |
| 211 | $sesskey = bin2hex(openssl_random_pseudo_bytes(512/8)); // Get 512 bits of randomness (128 byte hex string). |
| 212 | setcookie('sessionkey', $sesskey, 0, "", "", !$running_on_localhost, true); // Last two params are secure and httponly, secure is not set on localhost. |
| 213 | $result = $db->prepare('INSERT INTO `auth_sessions` (`sesskey`, `time_expire`) VALUES (:sesskey, :expire);'); |
| 214 | $result->execute(array(':sesskey' => $sesskey, ':expire' => gmdate('Y-m-d H:i:s', strtotime('+5 minutes')))); |
| 215 | // After insert, actually fetch the session row from the DB so we have all values. |
| 216 | $result = $db->prepare('SELECT * FROM auth_sessions WHERE `sesskey` = :sesskey AND `time_expire` > :expire;'); |
| 217 | $result->execute(array(':sesskey' => $sesskey, ':expire' => gmdate('Y-m-d H:i:s'))); |
| 218 | $row = $result->fetch(PDO::FETCH_ASSOC); |
| 219 | if ($row) { |
| 220 | $session = $row; |
| 221 | } |
| 222 | else { |
| 223 | // XXXlog: unexpected failure to create session! |
| 224 | $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.'); |
| 225 | } |
| 226 | } |
| 227 | } |
| 228 | |
| 229 | if (!count($errors)) { |
| 230 | if ($pagetype == 'verification_sent') { |
| 231 | $para = $body->appendElement('p', sprintf(_('An email for confirmation has been sent to %s. Please follow the link provided there to complete the process.'), $user['email'])); |
| 232 | $para->setAttribute('class', 'verifyinfo pending'); |
| 233 | } |
| 234 | elseif ($pagetype == 'resetstart') { |
| 235 | $para = $body->appendElement('p', _('If you forgot your password or didn\'t receive the registration confirmation, please enter your email here.')); |
| 236 | $para->setAttribute('class', ''); |
| 237 | $form = $body->appendForm('?reset', 'POST', 'resetform'); |
| 238 | $form->setAttribute('id', 'loginform'); |
| 239 | $form->setAttribute('class', 'loginarea hidden'); |
| 240 | $ulist = $form->appendElement('ul'); |
| 241 | $ulist->setAttribute('class', 'flat login'); |
| 242 | $litem = $ulist->appendElement('li'); |
| 243 | $inptxt = $litem->appendInputEmail('email', 30, 20, 'login_email'); |
| 244 | $inptxt->setAttribute('autocomplete', 'email'); |
| 245 | $inptxt->setAttribute('required', ''); |
| 246 | $inptxt->setAttribute('placeholder', _('Email')); |
| 247 | $litem = $ulist->appendElement('li'); |
| 248 | $submit = $litem->appendInputSubmit(_('Send instructions to email')); |
| 249 | } |
| 250 | elseif ($pagetype == 'resetpwd') { |
| 251 | $para = $body->appendElement('p', _('You can set a new password here.')); |
| 252 | $para->setAttribute('class', ''); |
| 253 | $form = $body->appendForm('?reset', 'POST', 'newpwdform'); |
| 254 | $form->setAttribute('id', 'loginform'); |
| 255 | $form->setAttribute('class', 'loginarea hidden'); |
| 256 | $ulist = $form->appendElement('ul'); |
| 257 | $ulist->setAttribute('class', 'flat login'); |
| 258 | $litem = $ulist->appendElement('li'); |
| 259 | $inptxt = $litem->appendInputPassword('pwd', 20, 20, 'login_pwd', ''); |
| 260 | $inptxt->setAttribute('required', ''); |
| 261 | $inptxt->setAttribute('placeholder', _('Password')); |
| 262 | $inptxt->setAttribute('class', 'login'); |
| 263 | $litem = $ulist->appendElement('li'); |
| 264 | $submit = $litem->appendInputSubmit(_('Save password')); |
| 265 | } |
| 266 | elseif ($session['logged_in']) { |
| 267 | $div = $body->appendElement('div', $user['email']); |
| 268 | $div->setAttribute('class', 'loginheader'); |
| 269 | $div = $body->appendElement('div'); |
| 270 | $div->setAttribute('class', 'loginlinks'); |
| 271 | $ulist = $div->appendElement('ul'); |
| 272 | $ulist->setAttribute('class', 'flat'); |
| 273 | $litem = $ulist->appendElement('li'); |
| 274 | $link = $litem->appendLink('?logout', _('Log out')); |
| 275 | $litem = $ulist->appendElement('li'); |
| 276 | $litem->appendLink('?reset', _('Set new password')); |
| 277 | } |
| 278 | else { // not logged in |
| 279 | if ($pagetype == 'verification_done') { |
| 280 | $para = $body->appendElement('p', _('Hooray! Your email was successfully confirmed! You can log in now.')); |
| 281 | $para->setAttribute('class', 'verifyinfo done'); |
| 282 | } |
| 283 | $form = $body->appendForm('?', 'POST', 'loginform'); |
| 284 | $form->setAttribute('id', 'loginform'); |
| 285 | $form->setAttribute('class', 'loginarea hidden'); |
| 286 | $ulist = $form->appendElement('ul'); |
| 287 | $ulist->setAttribute('class', 'flat login'); |
| 288 | $litem = $ulist->appendElement('li'); |
| 289 | $inptxt = $litem->appendInputEmail('email', 30, 20, 'login_email', (intval($user['id'])?$user['email']:'')); |
| 290 | $inptxt->setAttribute('autocomplete', 'email'); |
| 291 | $inptxt->setAttribute('required', ''); |
| 292 | $inptxt->setAttribute('placeholder', _('Email')); |
| 293 | $inptxt->setAttribute('class', 'login'); |
| 294 | $litem = $ulist->appendElement('li'); |
| 295 | $inptxt = $litem->appendInputPassword('pwd', 20, 20, 'login_pwd', ''); |
| 296 | $inptxt->setAttribute('required', ''); |
| 297 | $inptxt->setAttribute('placeholder', _('Password')); |
| 298 | $inptxt->setAttribute('class', 'login'); |
| 299 | $litem = $ulist->appendElement('li'); |
| 300 | $litem->appendLink('?reset', _('Forgot password?')); |
| 301 | $litem = $ulist->appendElement('li'); |
| 302 | $cbox = $litem->appendInputCheckbox('remember', 'login_remember', 'true', false); |
| 303 | $cbox->setAttribute('class', 'logincheck'); |
| 304 | $label = $litem->appendLabel('login_remember', _('Remember me')); |
| 305 | $label->setAttribute('id', 'rememprompt'); |
| 306 | $label->setAttribute('class', 'loginprompt'); |
| 307 | $litem = $ulist->appendElement('li'); |
| 308 | $submit = $litem->appendInputSubmit(_('Log in')); |
| 309 | $submit->setAttribute('class', 'loginbutton'); |
| 310 | } |
| 311 | } |
| 312 | |
| 313 | if (count($errors)) { |
| 314 | $body->appendElement('p', ((count($errors) <= 1) |
| 315 | ?_('The following error was detected') |
| 316 | :_('The following errors were detected')).':'); |
| 317 | $list = $body->appendElement('ul'); |
| 318 | $list->setAttribute('class', 'flat warn'); |
| 319 | foreach ($errors as $msg) { |
| 320 | $item = $list->appendElement('li', $msg); |
| 321 | } |
| 322 | $body->appendButton(_('Back'), 'history.back();'); |
| 323 | } |
| 324 | |
| 325 | // Send HTML to client. |
| 326 | print($document->saveHTML()); |
| 327 | ?> |